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