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