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 ranking::{find_cross_key_similar, SimilarFact};
12pub use types::*;
13
14#[cfg(test)]
15mod tests {
16 use super::*;
17 use crate::core::memory_boundary::FactPrivacy;
18 use crate::core::memory_policy::MemoryPolicy;
19 use chrono::Utc;
20
21 fn default_policy() -> MemoryPolicy {
22 MemoryPolicy::default()
23 }
24
25 #[test]
26 fn remember_and_recall() {
27 let policy = default_policy();
28 let mut k = ProjectKnowledge::new("/tmp/test-project");
29 k.remember(
30 "architecture",
31 "auth",
32 "JWT RS256",
33 "session-1",
34 0.9,
35 &policy,
36 );
37 k.remember("api", "rate-limit", "100/min", "session-1", 0.8, &policy);
38
39 let results = k.recall("auth");
40 assert_eq!(results.len(), 1);
41 assert_eq!(results[0].value, "JWT RS256");
42
43 let results = k.recall("api rate");
44 assert_eq!(results.len(), 1);
45 assert_eq!(results[0].key, "rate-limit");
46 }
47
48 #[test]
49 fn upsert_existing_fact() {
50 let policy = default_policy();
51 let mut k = ProjectKnowledge::new("/tmp/test");
52 k.remember("arch", "db", "PostgreSQL", "s1", 0.7, &policy);
53 k.remember(
54 "arch",
55 "db",
56 "PostgreSQL 16 with pgvector",
57 "s2",
58 0.95,
59 &policy,
60 );
61
62 let current: Vec<_> = k.facts.iter().filter(|f| f.is_current()).collect();
63 assert_eq!(current.len(), 1);
64 assert_eq!(current[0].value, "PostgreSQL 16 with pgvector");
65 }
66
67 #[test]
68 fn contradiction_detection() {
69 let policy = default_policy();
70 let mut k = ProjectKnowledge::new("/tmp/test");
71 k.remember("arch", "db", "PostgreSQL", "s1", 0.95, &policy);
72 k.facts[0].confirmation_count = 3;
73
74 let contradiction = k.check_contradiction("arch", "db", "MySQL", &policy);
75 assert!(contradiction.is_some());
76 let c = contradiction.unwrap();
77 assert_eq!(c.severity, ContradictionSeverity::High);
78 }
79
80 #[test]
81 fn temporal_validity() {
82 let policy = default_policy();
83 let mut k = ProjectKnowledge::new("/tmp/test");
84 k.remember("arch", "db", "PostgreSQL", "s1", 0.95, &policy);
85 k.facts[0].confirmation_count = 3;
86
87 k.remember("arch", "db", "MySQL", "s2", 0.9, &policy);
88
89 let current: Vec<_> = k.facts.iter().filter(|f| f.is_current()).collect();
90 assert_eq!(current.len(), 1);
91 assert_eq!(current[0].value, "MySQL");
92
93 let all_db: Vec<_> = k.facts.iter().filter(|f| f.key == "db").collect();
94 assert_eq!(all_db.len(), 2);
95 }
96
97 #[test]
98 fn confirmation_count() {
99 let policy = default_policy();
100 let mut k = ProjectKnowledge::new("/tmp/test");
101 k.remember("arch", "db", "PostgreSQL", "s1", 0.9, &policy);
102 assert_eq!(k.facts[0].confirmation_count, 1);
103
104 k.remember("arch", "db", "PostgreSQL", "s2", 0.9, &policy);
105 assert_eq!(k.facts[0].confirmation_count, 2);
106 }
107
108 #[test]
109 fn remove_fact() {
110 let policy = default_policy();
111 let mut k = ProjectKnowledge::new("/tmp/test");
112 k.remember("arch", "db", "PostgreSQL", "s1", 0.9, &policy);
113 assert!(k.remove_fact("arch", "db"));
114 assert!(k.facts.is_empty());
115 assert!(!k.remove_fact("arch", "db"));
116 }
117
118 #[test]
119 fn list_rooms() {
120 let policy = default_policy();
121 let mut k = ProjectKnowledge::new("/tmp/test");
122 k.remember("architecture", "auth", "JWT", "s1", 0.9, &policy);
123 k.remember("architecture", "db", "PG", "s1", 0.9, &policy);
124 k.remember("deploy", "host", "AWS", "s1", 0.8, &policy);
125
126 let rooms = k.list_rooms();
127 assert_eq!(rooms.len(), 2);
128 }
129
130 #[test]
131 fn aaak_format() {
132 let policy = default_policy();
133 let mut k = ProjectKnowledge::new("/tmp/test");
134 k.remember("architecture", "auth", "JWT RS256", "s1", 0.95, &policy);
135 k.remember("architecture", "db", "PostgreSQL", "s1", 0.7, &policy);
136
137 let aaak = k.format_aaak();
138 assert!(aaak.contains("ARCHITECTURE:"));
139 assert!(aaak.contains("auth=JWT RS256"));
140 }
141
142 #[test]
143 fn consolidate_history() {
144 let policy = default_policy();
145 let mut k = ProjectKnowledge::new("/tmp/test");
146 k.consolidate(
147 "Migrated from REST to GraphQL",
148 vec!["s1".into(), "s2".into()],
149 &policy,
150 );
151 assert_eq!(k.history.len(), 1);
152 assert_eq!(k.history[0].from_sessions.len(), 2);
153 }
154
155 #[test]
156 fn format_summary_output() {
157 let policy = default_policy();
158 let mut k = ProjectKnowledge::new("/tmp/test");
159 k.remember("architecture", "auth", "JWT RS256", "s1", 0.9, &policy);
160 k.add_pattern(
161 "naming",
162 "snake_case for functions",
163 vec!["get_user()".into()],
164 "s1",
165 &policy,
166 );
167 let summary = k.format_summary();
168 assert!(summary.contains("PROJECT KNOWLEDGE:"));
169 assert!(summary.contains("auth: JWT RS256"));
170 assert!(summary.contains("PROJECT PATTERNS:"));
171 }
172
173 #[test]
174 fn temporal_recall_at_time() {
175 let policy = default_policy();
176 let mut k = ProjectKnowledge::new("/tmp/test");
177 k.remember("arch", "db", "PostgreSQL", "s1", 0.95, &policy);
178 k.facts[0].confirmation_count = 3;
179
180 let before_change = Utc::now();
181 std::thread::sleep(std::time::Duration::from_millis(10));
182
183 k.remember("arch", "db", "MySQL", "s2", 0.9, &policy);
184
185 let results = k.recall_at_time("db", before_change);
186 assert_eq!(results.len(), 1);
187 assert_eq!(results[0].value, "PostgreSQL");
188
189 let results_now = k.recall_at_time("db", Utc::now());
190 assert_eq!(results_now.len(), 1);
191 assert_eq!(results_now[0].value, "MySQL");
192 }
193
194 #[test]
195 fn timeline_shows_history() {
196 let policy = default_policy();
197 let mut k = ProjectKnowledge::new("/tmp/test");
198 k.remember("arch", "db", "PostgreSQL", "s1", 0.95, &policy);
199 k.facts[0].confirmation_count = 3;
200 k.remember("arch", "db", "MySQL", "s2", 0.9, &policy);
201
202 let timeline = k.timeline("arch");
203 assert_eq!(timeline.len(), 2);
204 assert!(!timeline[0].is_current());
205 assert!(timeline[1].is_current());
206 }
207
208 #[test]
209 fn wakeup_format() {
210 let policy = default_policy();
211 let mut k = ProjectKnowledge::new("/tmp/test");
212 k.remember("arch", "auth", "JWT", "s1", 0.95, &policy);
213 k.remember("arch", "db", "PG", "s1", 0.8, &policy);
214
215 let wakeup = k.format_wakeup();
216 assert!(wakeup.contains("FACTS:"));
217 assert!(wakeup.contains("arch/auth=JWT"));
218 assert!(wakeup.contains("arch/db=PG"));
219 }
220
221 #[test]
222 fn salience_prioritizes_decisions_over_findings_at_similar_confidence() {
223 let policy = default_policy();
224 let mut k = ProjectKnowledge::new("/tmp/test");
225 k.remember("finding", "f1", "some thing", "s1", 0.9, &policy);
226 k.remember("decision", "d1", "important", "s1", 0.85, &policy);
227
228 let wakeup = k.format_wakeup();
229 let items = wakeup
230 .strip_prefix("FACTS:")
231 .unwrap_or(&wakeup)
232 .split('|')
233 .collect::<Vec<_>>();
234 assert!(
235 items
236 .first()
237 .is_some_and(|s| s.contains("decision/d1=important")),
238 "expected decision first in wakeup: {wakeup}"
239 );
240 }
241
242 #[test]
243 fn low_confidence_contradiction() {
244 let policy = default_policy();
245 let mut k = ProjectKnowledge::new("/tmp/test");
246 k.remember("arch", "db", "PostgreSQL", "s1", 0.4, &policy);
247
248 let c = k.check_contradiction("arch", "db", "MySQL", &policy);
249 assert!(c.is_some());
250 assert_eq!(c.unwrap().severity, ContradictionSeverity::Low);
251 }
252
253 #[test]
254 fn no_contradiction_for_same_value() {
255 let policy = default_policy();
256 let mut k = ProjectKnowledge::new("/tmp/test");
257 k.remember("arch", "db", "PostgreSQL", "s1", 0.95, &policy);
258
259 let c = k.check_contradiction("arch", "db", "PostgreSQL", &policy);
260 assert!(c.is_none());
261 }
262
263 #[test]
264 fn no_contradiction_for_similar_values() {
265 let policy = default_policy();
266 let mut k = ProjectKnowledge::new("/tmp/test");
267 k.remember(
268 "arch",
269 "db",
270 "PostgreSQL 16 production database server",
271 "s1",
272 0.95,
273 &policy,
274 );
275
276 let c = k.check_contradiction(
277 "arch",
278 "db",
279 "PostgreSQL 16 production database server config",
280 &policy,
281 );
282 assert!(c.is_none());
283 }
284
285 #[test]
286 fn import_skip_existing() {
287 let policy = default_policy();
288 let mut k = ProjectKnowledge::new("/tmp/test");
289 k.remember("arch", "db", "PostgreSQL", "s1", 0.95, &policy);
290
291 let incoming = vec![KnowledgeFact {
292 category: "arch".into(),
293 key: "db".into(),
294 value: "MySQL".into(),
295 source_session: "import".into(),
296 confidence: 0.8,
297 created_at: Utc::now(),
298 last_confirmed: Utc::now(),
299 retrieval_count: 0,
300 last_retrieved: None,
301 valid_from: Some(Utc::now()),
302 valid_until: None,
303 supersedes: None,
304 confirmation_count: 1,
305 feedback_up: 0,
306 feedback_down: 0,
307 last_feedback: None,
308 privacy: FactPrivacy::default(),
309 imported_from: None,
310 archetype: KnowledgeArchetype::default(),
311 fidelity: None,
312 revision_count: 0,
313 }];
314
315 let result = k.import_facts(incoming, ImportMerge::SkipExisting, "imp-1", &policy);
316 assert_eq!(result.skipped, 1);
317 assert_eq!(result.added, 0);
318 assert_eq!(k.facts.iter().filter(|f| f.is_current()).count(), 1);
319 }
320
321 #[test]
322 fn import_replace_existing() {
323 let policy = default_policy();
324 let mut k = ProjectKnowledge::new("/tmp/test");
325 k.remember("arch", "db", "PostgreSQL", "s1", 0.95, &policy);
326
327 let incoming = vec![KnowledgeFact {
328 category: "arch".into(),
329 key: "db".into(),
330 value: "MySQL".into(),
331 source_session: "import".into(),
332 confidence: 0.8,
333 created_at: Utc::now(),
334 last_confirmed: Utc::now(),
335 retrieval_count: 0,
336 last_retrieved: None,
337 valid_from: Some(Utc::now()),
338 valid_until: None,
339 supersedes: None,
340 confirmation_count: 1,
341 feedback_up: 0,
342 feedback_down: 0,
343 last_feedback: None,
344 privacy: FactPrivacy::default(),
345 imported_from: None,
346 archetype: KnowledgeArchetype::default(),
347 fidelity: None,
348 revision_count: 0,
349 }];
350
351 let result = k.import_facts(incoming, ImportMerge::Replace, "imp-1", &policy);
352 assert_eq!(result.replaced, 1);
353 let current: Vec<_> = k.facts.iter().filter(|f| f.is_current()).collect();
354 assert_eq!(current.len(), 1);
355 assert_eq!(current[0].value, "MySQL");
356 }
357
358 #[test]
359 fn import_adds_new_facts() {
360 let policy = default_policy();
361 let mut k = ProjectKnowledge::new("/tmp/test");
362 k.remember("arch", "db", "PostgreSQL", "s1", 0.95, &policy);
363
364 let incoming = vec![KnowledgeFact {
365 category: "security".into(),
366 key: "auth".into(),
367 value: "JWT".into(),
368 source_session: "import".into(),
369 confidence: 0.9,
370 created_at: Utc::now(),
371 last_confirmed: Utc::now(),
372 retrieval_count: 0,
373 last_retrieved: None,
374 valid_from: Some(Utc::now()),
375 valid_until: None,
376 supersedes: None,
377 confirmation_count: 1,
378 feedback_up: 0,
379 feedback_down: 0,
380 last_feedback: None,
381 privacy: FactPrivacy::default(),
382 imported_from: None,
383 archetype: KnowledgeArchetype::default(),
384 fidelity: None,
385 revision_count: 0,
386 }];
387
388 let result = k.import_facts(incoming, ImportMerge::SkipExisting, "imp-1", &policy);
389 assert_eq!(result.added, 1);
390 assert_eq!(k.facts.iter().filter(|f| f.is_current()).count(), 2);
391 }
392
393 #[test]
394 fn parse_simple_json_array() {
395 let data = r#"[
396 {"category": "arch", "key": "db", "value": "PostgreSQL"},
397 {"category": "security", "key": "auth", "value": "JWT", "confidence": 0.9}
398 ]"#;
399 let facts = parse_import_data(data).unwrap();
400 assert_eq!(facts.len(), 2);
401 assert_eq!(facts[0].category, "arch");
402 assert_eq!(facts[1].confidence, 0.9);
403 }
404
405 #[test]
406 fn parse_jsonl_format() {
407 let data = "{\"category\":\"arch\",\"key\":\"db\",\"value\":\"PG\"}\n\
408 {\"category\":\"security\",\"key\":\"auth\",\"value\":\"JWT\"}";
409 let facts = parse_import_data(data).unwrap();
410 assert_eq!(facts.len(), 2);
411 }
412
413 #[test]
414 fn export_simple_only_current() {
415 let policy = default_policy();
416 let mut k = ProjectKnowledge::new("/tmp/test");
417 k.remember("arch", "db", "PostgreSQL", "s1", 0.95, &policy);
418 k.remember("arch", "db", "MySQL", "s2", 0.9, &policy);
419
420 let exported = k.export_simple();
421 assert_eq!(exported.len(), 1);
422 assert_eq!(exported[0].value, "MySQL");
423 }
424
425 #[test]
426 fn import_merge_parse() {
427 assert_eq!(ImportMerge::parse("replace"), Some(ImportMerge::Replace));
428 assert_eq!(ImportMerge::parse("append"), Some(ImportMerge::Append));
429 assert_eq!(
430 ImportMerge::parse("skip-existing"),
431 Some(ImportMerge::SkipExisting)
432 );
433 assert_eq!(
434 ImportMerge::parse("skip_existing"),
435 Some(ImportMerge::SkipExisting)
436 );
437 assert_eq!(ImportMerge::parse("skip"), Some(ImportMerge::SkipExisting));
438 assert!(ImportMerge::parse("invalid").is_none());
439 }
440
441 #[test]
442 fn revision_count_on_new_fact() {
443 let policy = default_policy();
444 let mut k = ProjectKnowledge::new("/tmp/test");
445 k.remember("arch", "db", "PostgreSQL", "s1", 0.9, &policy);
446 let cur = k.facts.iter().find(|f| f.is_current()).unwrap();
447 assert_eq!(cur.revision_count, 1);
448 }
449
450 #[test]
451 fn revision_count_increments_on_confirm() {
452 let policy = default_policy();
453 let mut k = ProjectKnowledge::new("/tmp/test");
454 k.remember("arch", "db", "PostgreSQL", "s1", 0.9, &policy);
455 k.remember("arch", "db", "PostgreSQL", "s2", 0.9, &policy);
456 k.remember("arch", "db", "PostgreSQL", "s3", 0.9, &policy);
457 let cur = k.facts.iter().find(|f| f.is_current()).unwrap();
458 assert_eq!(cur.revision_count, 3);
459 assert_eq!(cur.confirmation_count, 3);
460 }
461
462 #[test]
463 fn revision_count_carries_over_on_supersede() {
464 let policy = default_policy();
465 let mut k = ProjectKnowledge::new("/tmp/test");
466 k.remember("arch", "db", "PostgreSQL", "s1", 0.95, &policy);
467 k.remember("arch", "db", "PostgreSQL", "s2", 0.9, &policy);
468 assert_eq!(
469 k.facts
470 .iter()
471 .find(|f| f.is_current())
472 .unwrap()
473 .revision_count,
474 2
475 );
476 k.facts[0].confirmation_count = 3;
477 k.remember("arch", "db", "MySQL", "s3", 0.9, &policy);
478 let cur: Vec<_> = k.facts.iter().filter(|f| f.is_current()).collect();
479 assert_eq!(cur.len(), 1);
480 assert_eq!(cur[0].value, "MySQL");
481 assert_eq!(cur[0].revision_count, 3);
482 assert!(cur[0].supersedes.is_some());
483 }
484
485 #[test]
486 fn revision_count_default_zero_for_legacy() {
487 let json = r#"{
488 "category": "test", "key": "k", "value": "v",
489 "source_session": "s", "confidence": 0.8,
490 "created_at": "2024-01-01T00:00:00Z",
491 "last_confirmed": "2024-01-01T00:00:00Z"
492 }"#;
493 let fact: KnowledgeFact = serde_json::from_str(json).unwrap();
494 assert_eq!(fact.revision_count, 0);
495 }
496
497 #[test]
498 fn judged_pairs_default_empty_for_legacy() {
499 let json = r#"{
500 "project_root": "/test", "project_hash": "abc",
501 "facts": [], "patterns": [], "history": [],
502 "updated_at": "2024-01-01T00:00:00Z"
503 }"#;
504 let pk: ProjectKnowledge = serde_json::from_str(json).unwrap();
505 assert!(pk.judged_pairs.is_empty());
506 }
507
508 #[test]
509 fn cross_key_similar_finds_related_facts() {
510 let policy = default_policy();
511 let mut k = ProjectKnowledge::new("/tmp/test");
512 k.remember(
513 "architecture",
514 "auth",
515 "JWT RS256 token based authentication with Redis session store",
516 "s1",
517 0.9,
518 &policy,
519 );
520 k.remember(
521 "decision",
522 "session-model",
523 "JWT token authentication stored in Redis for session management",
524 "s1",
525 0.85,
526 &policy,
527 );
528 k.remember("deploy", "host", "AWS us-east-1", "s1", 0.8, &policy);
529
530 let similar = find_cross_key_similar(
531 "architecture",
532 "auth",
533 "JWT RS256 token based authentication with Redis session store",
534 &k.facts,
535 &k.judged_pairs,
536 3,
537 );
538 assert!(!similar.is_empty(), "should find session-model as similar");
539 assert_eq!(similar[0].category, "decision");
540 assert_eq!(similar[0].key, "session-model");
541 assert!(similar[0].similarity > 0.35);
542 }
543
544 #[test]
545 fn cross_key_similar_excludes_same_key() {
546 let policy = default_policy();
547 let mut k = ProjectKnowledge::new("/tmp/test");
548 k.remember("arch", "db", "PostgreSQL 16", "s1", 0.9, &policy);
549
550 let similar =
551 find_cross_key_similar("arch", "db", "PostgreSQL 16", &k.facts, &k.judged_pairs, 3);
552 assert!(similar.is_empty());
553 }
554
555 #[test]
556 fn cross_key_similar_excludes_judged_pairs() {
557 let policy = default_policy();
558 let mut k = ProjectKnowledge::new("/tmp/test");
559 k.remember(
560 "architecture",
561 "auth",
562 "JWT RS256 token based authentication with Redis",
563 "s1",
564 0.9,
565 &policy,
566 );
567 k.remember(
568 "decision",
569 "session-model",
570 "JWT token authentication stored in Redis",
571 "s1",
572 0.85,
573 &policy,
574 );
575
576 k.judged_pairs.push(JudgedPair {
577 key_a: "architecture/auth".into(),
578 key_b: "decision/session-model".into(),
579 verdict: "compatible".into(),
580 judged_at: Utc::now(),
581 });
582
583 let similar = find_cross_key_similar(
584 "architecture",
585 "auth",
586 "JWT RS256 token based authentication with Redis",
587 &k.facts,
588 &k.judged_pairs,
589 3,
590 );
591 assert!(similar.is_empty(), "judged pairs should be excluded");
592 }
593
594 #[test]
595 fn cross_key_similar_ignores_unrelated_facts() {
596 let policy = default_policy();
597 let mut k = ProjectKnowledge::new("/tmp/test");
598 k.remember(
599 "arch",
600 "db",
601 "PostgreSQL 16 with pgvector",
602 "s1",
603 0.9,
604 &policy,
605 );
606 k.remember("deploy", "host", "AWS us-east-1 region", "s1", 0.8, &policy);
607
608 let similar = find_cross_key_similar(
609 "arch",
610 "db",
611 "PostgreSQL 16 with pgvector",
612 &k.facts,
613 &k.judged_pairs,
614 3,
615 );
616 assert!(similar.is_empty(), "unrelated facts should not match");
617 }
618
619 #[test]
620 fn judge_supersedes_archives_target() {
621 let policy = default_policy();
622 let mut k = ProjectKnowledge::new("/tmp/test");
623 k.remember("architecture", "auth", "JWT RS256", "s1", 0.9, &policy);
624 k.remember("decision", "session", "JWT tokens", "s1", 0.85, &policy);
625
626 assert!(k.facts.iter().all(KnowledgeFact::is_current));
627
628 if let Some(tf) = k
629 .facts
630 .iter_mut()
631 .find(|f| f.category == "decision" && f.key == "session" && f.is_current())
632 {
633 tf.valid_until = Some(Utc::now());
634 }
635 k.judged_pairs.push(JudgedPair {
636 key_a: "architecture/auth".into(),
637 key_b: "decision/session".into(),
638 verdict: "supersedes".into(),
639 judged_at: Utc::now(),
640 });
641
642 let cur: Vec<_> = k.facts.iter().filter(|f| f.is_current()).collect();
643 assert_eq!(cur.len(), 1);
644 assert_eq!(cur[0].category, "architecture");
645 }
646}