1pub mod chunking;
2mod core;
3mod fact;
4mod format;
5mod import_export;
6pub mod maintenance;
7mod persist;
8mod query;
9mod ranking;
10mod types;
11
12pub use import_export::{ImportMerge, ImportResult, SimpleFactEntry, parse_import_data};
13pub use ranking::{SimilarFact, find_cross_key_similar};
14pub use types::*;
15
16#[cfg(test)]
17mod tests {
18 use super::*;
19 use crate::core::memory_boundary::FactPrivacy;
20 use crate::core::memory_policy::MemoryPolicy;
21 use chrono::Utc;
22
23 fn default_policy() -> MemoryPolicy {
24 MemoryPolicy::default()
25 }
26
27 fn current_count(k: &ProjectKnowledge) -> usize {
28 k.facts.iter().filter(|f| f.is_current()).count()
29 }
30
31 #[test]
34 fn admission_merges_same_category_near_duplicate_without_growing() {
35 let policy = default_policy(); let mut k = ProjectKnowledge::new("/tmp/test-admission-merge");
37
38 let r1 = k.remember_admitted(
39 "architecture",
40 "db-primary",
41 "use postgres for the primary datastore",
42 "s1",
43 0.8,
44 &policy,
45 );
46 assert!(matches!(r1, AdmissionResult::Stored(_)));
47 assert_eq!(current_count(&k), 1);
48
49 let r2 = k.remember_admitted(
51 "architecture",
52 "db-store",
53 "use postgres for the primary datastore",
54 "s2",
55 0.9,
56 &policy,
57 );
58 match r2 {
59 AdmissionResult::Merged {
60 category,
61 key,
62 confirmations,
63 ..
64 } => {
65 assert_eq!(category, "architecture");
66 assert_eq!(key, "db-primary", "merges into the earliest match");
67 assert_eq!(confirmations, 2);
68 }
69 other => panic!("expected Merged, got {other:?}"),
70 }
71 assert_eq!(current_count(&k), 1, "merge must not grow the store");
72 let f = k
73 .facts
74 .iter()
75 .find(|f| f.key == "db-primary" && f.is_current())
76 .unwrap();
77 assert_eq!(f.confirmation_count, 2);
78 }
79
80 #[test]
81 fn admission_rejects_below_salience_floor() {
82 let mut policy = default_policy();
83 policy.admission.min_salience = 100; let mut k = ProjectKnowledge::new("/tmp/test-admission-floor");
86 let r = k.remember_admitted("misc", "note", "a quiet plain note", "s1", 0.8, &policy);
87 assert!(matches!(r, AdmissionResult::RejectedLowSalience { .. }));
88 assert_eq!(current_count(&k), 0, "rejected fact is never stored");
89
90 let r2 = k.remember_admitted(
92 "misc",
93 "bug",
94 "security error: auth failed with timeout",
95 "s1",
96 0.8,
97 &policy,
98 );
99 assert!(matches!(r2, AdmissionResult::Stored(_)));
100 assert_eq!(current_count(&k), 1);
101 }
102
103 #[test]
104 fn admission_preserves_contradiction_supersede_on_same_key() {
105 let policy = default_policy();
106 let mut k = ProjectKnowledge::new("/tmp/test-admission-contradiction");
107
108 let r1 = k.remember_admitted("arch", "db", "PostgreSQL", "s1", 0.95, &policy);
109 assert!(matches!(r1, AdmissionResult::Stored(None)));
110
111 let r2 = k.remember_admitted("arch", "db", "MySQL", "s2", 0.9, &policy);
113 assert!(
114 matches!(r2, AdmissionResult::Stored(Some(_))),
115 "same-key conflict must surface a contradiction, got {r2:?}"
116 );
117 let cur = k
118 .facts
119 .iter()
120 .find(|f| f.category == "arch" && f.key == "db" && f.is_current())
121 .unwrap();
122 assert_eq!(cur.value, "MySQL");
123 }
124
125 #[test]
126 fn admission_disabled_inserts_like_legacy() {
127 let mut policy = default_policy();
128 policy.admission.enabled = false;
129
130 let mut k = ProjectKnowledge::new("/tmp/test-admission-off");
131 k.remember_admitted(
132 "architecture",
133 "a",
134 "use postgres for the primary datastore",
135 "s1",
136 0.8,
137 &policy,
138 );
139 let r = k.remember_admitted(
140 "architecture",
141 "b",
142 "use postgres for the primary datastore",
143 "s2",
144 0.9,
145 &policy,
146 );
147 assert!(
148 matches!(r, AdmissionResult::Stored(_)),
149 "disabled admission never merges"
150 );
151 assert_eq!(current_count(&k), 2, "both distinct keys stored");
152 }
153
154 #[test]
155 fn admission_does_not_merge_across_categories() {
156 let policy = default_policy();
157 let mut k = ProjectKnowledge::new("/tmp/test-admission-xcat");
158
159 k.remember_admitted(
160 "architecture",
161 "db",
162 "use postgres for the primary datastore",
163 "s1",
164 0.8,
165 &policy,
166 );
167 let r = k.remember_admitted(
169 "decision",
170 "db",
171 "use postgres for the primary datastore",
172 "s2",
173 0.9,
174 &policy,
175 );
176 assert!(matches!(r, AdmissionResult::Stored(_)));
177 assert_eq!(current_count(&k), 2);
178 }
179
180 #[test]
181 fn remember_and_recall() {
182 let policy = default_policy();
183 let mut k = ProjectKnowledge::new("/tmp/test-project");
184 k.remember(
185 "architecture",
186 "auth",
187 "JWT RS256",
188 "session-1",
189 0.9,
190 &policy,
191 );
192 k.remember("api", "rate-limit", "100/min", "session-1", 0.8, &policy);
193
194 let results = k.recall("auth");
195 assert_eq!(results.len(), 1);
196 assert_eq!(results[0].value, "JWT RS256");
197
198 let results = k.recall("api rate");
199 assert_eq!(results.len(), 1);
200 assert_eq!(results[0].key, "rate-limit");
201 }
202
203 #[test]
204 fn remember_infers_archetype_from_category() {
205 let policy = default_policy();
206 let mut k = ProjectKnowledge::new("/tmp/test-archetype");
207 k.remember("architecture", "db", "PostgreSQL", "s1", 0.9, &policy);
208 k.remember("decision", "lang", "Rust", "s1", 0.9, &policy);
209 k.remember("known_bugs", "src/a.rs:1", "panics", "s1", 0.9, &policy);
210 k.remember("data_model", "user", "id,email", "s1", 0.9, &policy);
211 k.remember("misc", "note", "whatever", "s1", 0.9, &policy);
212
213 let arch = |cat: &str| {
214 k.facts
215 .iter()
216 .find(|f| f.category == cat && f.is_current())
217 .map(|f| f.archetype.clone())
218 .expect("fact present")
219 };
220 assert_eq!(arch("architecture"), KnowledgeArchetype::Architecture);
221 assert_eq!(arch("decision"), KnowledgeArchetype::Decision);
222 assert_eq!(arch("known_bugs"), KnowledgeArchetype::Gotcha);
223 assert_eq!(arch("data_model"), KnowledgeArchetype::Architecture);
224 assert_eq!(arch("misc"), KnowledgeArchetype::Fact);
225 }
226
227 #[test]
228 fn recall_surfaces_synthesized_observation_first() {
229 let policy = default_policy();
230 let mut k = ProjectKnowledge::new("/tmp/test-recall-obs");
231 k.remember(
233 "decision",
234 "auth-choice",
235 "auth module uses jwt tokens",
236 "s1",
237 0.6,
238 &policy,
239 );
240 k.remember(
241 "observation",
242 "src/auth.rs",
243 "auth module summary jwt tokens",
244 crate::core::knowledge::COGNITION_SYNTHESIS_SOURCE,
245 0.6,
246 &policy,
247 );
248
249 let (out, total) = k.recall_for_output("jwt", 10);
250 assert!(total >= 2, "both facts match the query");
251 assert!(
252 out[0].is_synthesized_observation(),
253 "synthesized observation ranks above an equally relevant raw fact"
254 );
255 assert_eq!(out[0].key, "src/auth.rs");
256 }
257
258 #[test]
259 fn recall_exact_key_hit_outranks_observation() {
260 let policy = default_policy();
264 let mut k = ProjectKnowledge::new("/tmp/test-recall-exact-vs-obs");
265 k.remember(
266 "decision",
267 "jwt",
268 "auth module uses jwt tokens",
269 "s1",
270 0.6,
271 &policy,
272 );
273 k.remember(
274 "observation",
275 "src/auth.rs",
276 "auth module summary jwt tokens",
277 crate::core::knowledge::COGNITION_SYNTHESIS_SOURCE,
278 0.6,
279 &policy,
280 );
281
282 let (out, _total) = k.recall_for_output("jwt", 10);
283 assert_eq!(out[0].key, "jwt", "exact key hit leads the ranking");
284 assert_eq!(out[0].category, "decision");
285 assert!(
286 out.iter()
287 .any(crate::core::knowledge::KnowledgeFact::is_synthesized_observation),
288 "the observation is still recalled, just below the exact hit"
289 );
290 }
291
292 #[test]
293 fn facts_evict_down_to_cap_not_double() {
294 let mut policy = default_policy();
298 policy.knowledge.max_facts = 5;
299 let mut k = ProjectKnowledge::new("/tmp/test-evict");
300 for i in 0..40 {
301 k.remember(
302 "finding",
303 &format!("key-{i}"),
304 &format!("value number {i}"),
305 "s1",
306 0.7,
307 &policy,
308 );
309 }
310 assert!(
311 k.facts.len() <= policy.knowledge.max_facts,
312 "expected <= {} facts after eviction, got {}",
313 policy.knowledge.max_facts,
314 k.facts.len()
315 );
316 }
317
318 #[test]
319 fn import_facts_evict_down_to_cap_not_double() {
320 let mut policy = default_policy();
326 policy.knowledge.max_facts = 5;
327
328 let mut source_policy = default_policy();
330 source_policy.knowledge.max_facts = 1000;
331 let mut source = ProjectKnowledge::new("/tmp/test-import-source");
332 for i in 0..40 {
333 source.remember(
334 "finding",
335 &format!("key-{i}"),
336 &format!("value number {i}"),
337 "s1",
338 0.7,
339 &source_policy,
340 );
341 }
342 let incoming = source.facts.clone();
343 assert!(
344 incoming.len() > policy.knowledge.max_facts * 2,
345 "test needs a batch larger than 2x cap to exercise the old guard, got {}",
346 incoming.len()
347 );
348
349 let mut k = ProjectKnowledge::new("/tmp/test-import-target");
350 k.import_facts(incoming, ImportMerge::Append, "s2", &policy);
351 assert!(
352 k.facts.len() <= policy.knowledge.max_facts,
353 "expected <= {} facts after import eviction, got {}",
354 policy.knowledge.max_facts,
355 k.facts.len()
356 );
357 }
358
359 #[test]
360 fn upsert_existing_fact() {
361 let policy = default_policy();
362 let mut k = ProjectKnowledge::new("/tmp/test");
363 k.remember("arch", "db", "PostgreSQL", "s1", 0.7, &policy);
364 k.remember(
365 "arch",
366 "db",
367 "PostgreSQL 16 with pgvector",
368 "s2",
369 0.95,
370 &policy,
371 );
372
373 let current: Vec<_> = k.facts.iter().filter(|f| f.is_current()).collect();
374 assert_eq!(current.len(), 1);
375 assert_eq!(current[0].value, "PostgreSQL 16 with pgvector");
376 }
377
378 #[test]
379 fn contradiction_detection() {
380 let policy = default_policy();
381 let mut k = ProjectKnowledge::new("/tmp/test");
382 k.remember("arch", "db", "PostgreSQL", "s1", 0.95, &policy);
383 k.facts[0].confirmation_count = 3;
384
385 let contradiction = k.check_contradiction("arch", "db", "MySQL", &policy);
386 assert!(contradiction.is_some());
387 let c = contradiction.unwrap();
388 assert_eq!(c.severity, ContradictionSeverity::High);
389 }
390
391 #[test]
392 fn temporal_validity() {
393 let policy = default_policy();
394 let mut k = ProjectKnowledge::new("/tmp/test");
395 k.remember("arch", "db", "PostgreSQL", "s1", 0.95, &policy);
396 k.facts[0].confirmation_count = 3;
397
398 k.remember("arch", "db", "MySQL", "s2", 0.9, &policy);
399
400 let current: Vec<_> = k.facts.iter().filter(|f| f.is_current()).collect();
401 assert_eq!(current.len(), 1);
402 assert_eq!(current[0].value, "MySQL");
403
404 let all_db: Vec<_> = k.facts.iter().filter(|f| f.key == "db").collect();
405 assert_eq!(all_db.len(), 2);
406 }
407
408 #[test]
409 fn confirmation_count() {
410 let policy = default_policy();
411 let mut k = ProjectKnowledge::new("/tmp/test");
412 k.remember("arch", "db", "PostgreSQL", "s1", 0.9, &policy);
413 assert_eq!(k.facts[0].confirmation_count, 1);
414
415 k.remember("arch", "db", "PostgreSQL", "s2", 0.9, &policy);
416 assert_eq!(k.facts[0].confirmation_count, 2);
417 }
418
419 #[test]
420 fn remove_fact() {
421 let policy = default_policy();
422 let mut k = ProjectKnowledge::new("/tmp/test");
423 k.remember("arch", "db", "PostgreSQL", "s1", 0.9, &policy);
424 assert!(k.remove_fact("arch", "db"));
425 assert!(k.facts.is_empty());
426 assert!(!k.remove_fact("arch", "db"));
427 }
428
429 #[test]
430 fn list_rooms() {
431 let policy = default_policy();
432 let mut k = ProjectKnowledge::new("/tmp/test");
433 k.remember("architecture", "auth", "JWT", "s1", 0.9, &policy);
434 k.remember("architecture", "db", "PG", "s1", 0.9, &policy);
435 k.remember("deploy", "host", "AWS", "s1", 0.8, &policy);
436
437 let rooms = k.list_rooms();
438 assert_eq!(rooms.len(), 2);
439 }
440
441 #[test]
442 fn aaak_format() {
443 let policy = default_policy();
444 let mut k = ProjectKnowledge::new("/tmp/test");
445 k.remember("architecture", "auth", "JWT RS256", "s1", 0.95, &policy);
446 k.remember("architecture", "db", "PostgreSQL", "s1", 0.7, &policy);
447
448 let aaak = k.format_aaak();
449 assert!(aaak.contains("ARCHITECTURE:"));
450 assert!(aaak.contains("auth=JWT RS256"));
451 }
452
453 #[test]
454 fn consolidate_history() {
455 let policy = default_policy();
456 let mut k = ProjectKnowledge::new("/tmp/test");
457 k.consolidate(
458 "Migrated from REST to GraphQL",
459 vec!["s1".into(), "s2".into()],
460 &policy,
461 );
462 assert_eq!(k.history.len(), 1);
463 assert_eq!(k.history[0].from_sessions.len(), 2);
464 }
465
466 #[test]
467 fn format_summary_output() {
468 let policy = default_policy();
469 let mut k = ProjectKnowledge::new("/tmp/test");
470 k.remember("architecture", "auth", "JWT RS256", "s1", 0.9, &policy);
471 k.add_pattern(
472 "naming",
473 "snake_case for functions",
474 vec!["get_user()".into()],
475 "s1",
476 &policy,
477 );
478 let summary = k.format_summary();
479 assert!(summary.contains("PROJECT KNOWLEDGE:"));
480 assert!(summary.contains("auth: JWT RS256"));
481 assert!(summary.contains("PROJECT PATTERNS:"));
482 }
483
484 #[test]
485 fn temporal_recall_at_time() {
486 let policy = default_policy();
487 let mut k = ProjectKnowledge::new("/tmp/test");
488 k.remember("arch", "db", "PostgreSQL", "s1", 0.95, &policy);
489 k.facts[0].confirmation_count = 3;
490
491 let before_change = Utc::now();
492 std::thread::sleep(std::time::Duration::from_millis(10));
493
494 k.remember("arch", "db", "MySQL", "s2", 0.9, &policy);
495
496 let results = k.recall_at_time("db", before_change);
497 assert_eq!(results.len(), 1);
498 assert_eq!(results[0].value, "PostgreSQL");
499
500 let results_now = k.recall_at_time("db", Utc::now());
501 assert_eq!(results_now.len(), 1);
502 assert_eq!(results_now[0].value, "MySQL");
503 }
504
505 #[test]
506 fn timeline_shows_history() {
507 let policy = default_policy();
508 let mut k = ProjectKnowledge::new("/tmp/test");
509 k.remember("arch", "db", "PostgreSQL", "s1", 0.95, &policy);
510 k.facts[0].confirmation_count = 3;
511 k.remember("arch", "db", "MySQL", "s2", 0.9, &policy);
512
513 let timeline = k.timeline("arch");
514 assert_eq!(timeline.len(), 2);
515 assert!(!timeline[0].is_current());
516 assert!(timeline[1].is_current());
517 }
518
519 #[test]
520 fn wakeup_format() {
521 let policy = default_policy();
522 let mut k = ProjectKnowledge::new("/tmp/test");
523 k.remember("arch", "auth", "JWT", "s1", 0.95, &policy);
524 k.remember("arch", "db", "PG", "s1", 0.8, &policy);
525
526 let wakeup = k.format_wakeup();
529 assert!(wakeup.contains("FACTS:"));
530 assert!(wakeup.contains("[arch]"));
531 assert!(wakeup.contains("auth=JWT"));
532 assert!(wakeup.contains("db=PG"));
533 }
534
535 #[test]
536 fn salience_prioritizes_decisions_over_findings_at_similar_confidence() {
537 let policy = default_policy();
538 let mut k = ProjectKnowledge::new("/tmp/test");
539 k.remember("finding", "f1", "some thing", "s1", 0.9, &policy);
540 k.remember("decision", "d1", "important", "s1", 0.85, &policy);
541
542 let wakeup = k.format_wakeup();
545 let d = wakeup.find("d1=important").expect("decision in wakeup");
546 let f = wakeup.find("f1=some thing").expect("finding in wakeup");
547 assert!(
548 d < f,
549 "expected decision before finding in wakeup: {wakeup}"
550 );
551 }
552
553 #[test]
554 fn low_confidence_contradiction() {
555 let policy = default_policy();
556 let mut k = ProjectKnowledge::new("/tmp/test");
557 k.remember("arch", "db", "PostgreSQL", "s1", 0.4, &policy);
558
559 let c = k.check_contradiction("arch", "db", "MySQL", &policy);
560 assert!(c.is_some());
561 assert_eq!(c.unwrap().severity, ContradictionSeverity::Low);
562 }
563
564 #[test]
565 fn no_contradiction_for_same_value() {
566 let policy = default_policy();
567 let mut k = ProjectKnowledge::new("/tmp/test");
568 k.remember("arch", "db", "PostgreSQL", "s1", 0.95, &policy);
569
570 let c = k.check_contradiction("arch", "db", "PostgreSQL", &policy);
571 assert!(c.is_none());
572 }
573
574 #[test]
575 fn no_contradiction_for_similar_values() {
576 let policy = default_policy();
577 let mut k = ProjectKnowledge::new("/tmp/test");
578 k.remember(
579 "arch",
580 "db",
581 "PostgreSQL 16 production database server",
582 "s1",
583 0.95,
584 &policy,
585 );
586
587 let c = k.check_contradiction(
588 "arch",
589 "db",
590 "PostgreSQL 16 production database server config",
591 &policy,
592 );
593 assert!(c.is_none());
594 }
595
596 #[test]
597 fn import_skip_existing() {
598 let policy = default_policy();
599 let mut k = ProjectKnowledge::new("/tmp/test");
600 k.remember("arch", "db", "PostgreSQL", "s1", 0.95, &policy);
601
602 let incoming = vec![KnowledgeFact {
603 category: "arch".into(),
604 key: "db".into(),
605 value: "MySQL".into(),
606 source_session: "import".into(),
607 confidence: 0.8,
608 created_at: Utc::now(),
609 last_confirmed: Utc::now(),
610 retrieval_count: 0,
611 last_retrieved: None,
612 valid_from: Some(Utc::now()),
613 valid_until: None,
614 supersedes: None,
615 confirmation_count: 1,
616 feedback_up: 0,
617 feedback_down: 0,
618 last_feedback: None,
619 privacy: FactPrivacy::default(),
620 sensitivity: crate::core::sensitivity::SensitivityLevel::default(),
621 imported_from: None,
622 archetype: KnowledgeArchetype::default(),
623 fidelity: None,
624 revision_count: 0,
625 }];
626
627 let result = k.import_facts(incoming, ImportMerge::SkipExisting, "imp-1", &policy);
628 assert_eq!(result.skipped, 1);
629 assert_eq!(result.added, 0);
630 assert_eq!(k.facts.iter().filter(|f| f.is_current()).count(), 1);
631 }
632
633 #[test]
634 fn import_replace_existing() {
635 let policy = default_policy();
636 let mut k = ProjectKnowledge::new("/tmp/test");
637 k.remember("arch", "db", "PostgreSQL", "s1", 0.95, &policy);
638
639 let incoming = vec![KnowledgeFact {
640 category: "arch".into(),
641 key: "db".into(),
642 value: "MySQL".into(),
643 source_session: "import".into(),
644 confidence: 0.8,
645 created_at: Utc::now(),
646 last_confirmed: Utc::now(),
647 retrieval_count: 0,
648 last_retrieved: None,
649 valid_from: Some(Utc::now()),
650 valid_until: None,
651 supersedes: None,
652 confirmation_count: 1,
653 feedback_up: 0,
654 feedback_down: 0,
655 last_feedback: None,
656 privacy: FactPrivacy::default(),
657 sensitivity: crate::core::sensitivity::SensitivityLevel::default(),
658 imported_from: None,
659 archetype: KnowledgeArchetype::default(),
660 fidelity: None,
661 revision_count: 0,
662 }];
663
664 let result = k.import_facts(incoming, ImportMerge::Replace, "imp-1", &policy);
665 assert_eq!(result.replaced, 1);
666 let current: Vec<_> = k.facts.iter().filter(|f| f.is_current()).collect();
667 assert_eq!(current.len(), 1);
668 assert_eq!(current[0].value, "MySQL");
669 }
670
671 #[test]
672 fn import_adds_new_facts() {
673 let policy = default_policy();
674 let mut k = ProjectKnowledge::new("/tmp/test");
675 k.remember("arch", "db", "PostgreSQL", "s1", 0.95, &policy);
676
677 let incoming = vec![KnowledgeFact {
678 category: "security".into(),
679 key: "auth".into(),
680 value: "JWT".into(),
681 source_session: "import".into(),
682 confidence: 0.9,
683 created_at: Utc::now(),
684 last_confirmed: Utc::now(),
685 retrieval_count: 0,
686 last_retrieved: None,
687 valid_from: Some(Utc::now()),
688 valid_until: None,
689 supersedes: None,
690 confirmation_count: 1,
691 feedback_up: 0,
692 feedback_down: 0,
693 last_feedback: None,
694 privacy: FactPrivacy::default(),
695 sensitivity: crate::core::sensitivity::SensitivityLevel::default(),
696 imported_from: None,
697 archetype: KnowledgeArchetype::default(),
698 fidelity: None,
699 revision_count: 0,
700 }];
701
702 let result = k.import_facts(incoming, ImportMerge::SkipExisting, "imp-1", &policy);
703 assert_eq!(result.added, 1);
704 assert_eq!(k.facts.iter().filter(|f| f.is_current()).count(), 2);
705 }
706
707 #[test]
708 fn parse_simple_json_array() {
709 let data = r#"[
710 {"category": "arch", "key": "db", "value": "PostgreSQL"},
711 {"category": "security", "key": "auth", "value": "JWT", "confidence": 0.9}
712 ]"#;
713 let facts = parse_import_data(data).unwrap();
714 assert_eq!(facts.len(), 2);
715 assert_eq!(facts[0].category, "arch");
716 assert_eq!(facts[1].confidence, 0.9);
717 }
718
719 #[test]
720 fn parse_jsonl_format() {
721 let data = "{\"category\":\"arch\",\"key\":\"db\",\"value\":\"PG\"}\n\
722 {\"category\":\"security\",\"key\":\"auth\",\"value\":\"JWT\"}";
723 let facts = parse_import_data(data).unwrap();
724 assert_eq!(facts.len(), 2);
725 }
726
727 #[test]
728 fn export_simple_only_current() {
729 let policy = default_policy();
730 let mut k = ProjectKnowledge::new("/tmp/test");
731 k.remember("arch", "db", "PostgreSQL", "s1", 0.95, &policy);
732 k.remember("arch", "db", "MySQL", "s2", 0.9, &policy);
733
734 let exported = k.export_simple();
735 assert_eq!(exported.len(), 1);
736 assert_eq!(exported[0].value, "MySQL");
737 }
738
739 #[test]
740 fn import_merge_parse() {
741 assert_eq!(ImportMerge::parse("replace"), Some(ImportMerge::Replace));
742 assert_eq!(ImportMerge::parse("append"), Some(ImportMerge::Append));
743 assert_eq!(
744 ImportMerge::parse("skip-existing"),
745 Some(ImportMerge::SkipExisting)
746 );
747 assert_eq!(
748 ImportMerge::parse("skip_existing"),
749 Some(ImportMerge::SkipExisting)
750 );
751 assert_eq!(ImportMerge::parse("skip"), Some(ImportMerge::SkipExisting));
752 assert!(ImportMerge::parse("invalid").is_none());
753 }
754
755 #[test]
756 fn revision_count_on_new_fact() {
757 let policy = default_policy();
758 let mut k = ProjectKnowledge::new("/tmp/test");
759 k.remember("arch", "db", "PostgreSQL", "s1", 0.9, &policy);
760 let cur = k.facts.iter().find(|f| f.is_current()).unwrap();
761 assert_eq!(cur.revision_count, 1);
762 }
763
764 #[test]
765 fn revision_count_increments_on_confirm() {
766 let policy = default_policy();
767 let mut k = ProjectKnowledge::new("/tmp/test");
768 k.remember("arch", "db", "PostgreSQL", "s1", 0.9, &policy);
769 k.remember("arch", "db", "PostgreSQL", "s2", 0.9, &policy);
770 k.remember("arch", "db", "PostgreSQL", "s3", 0.9, &policy);
771 let cur = k.facts.iter().find(|f| f.is_current()).unwrap();
772 assert_eq!(cur.revision_count, 3);
773 assert_eq!(cur.confirmation_count, 3);
774 }
775
776 #[test]
777 fn revision_count_carries_over_on_supersede() {
778 let policy = default_policy();
779 let mut k = ProjectKnowledge::new("/tmp/test");
780 k.remember("arch", "db", "PostgreSQL", "s1", 0.95, &policy);
781 k.remember("arch", "db", "PostgreSQL", "s2", 0.9, &policy);
782 assert_eq!(
783 k.facts
784 .iter()
785 .find(|f| f.is_current())
786 .unwrap()
787 .revision_count,
788 2
789 );
790 k.facts[0].confirmation_count = 3;
791 k.remember("arch", "db", "MySQL", "s3", 0.9, &policy);
792 let cur: Vec<_> = k.facts.iter().filter(|f| f.is_current()).collect();
793 assert_eq!(cur.len(), 1);
794 assert_eq!(cur[0].value, "MySQL");
795 assert_eq!(cur[0].revision_count, 3);
796 assert!(cur[0].supersedes.is_some());
797 }
798
799 #[test]
800 fn revision_count_default_zero_for_legacy() {
801 let json = r#"{
802 "category": "test", "key": "k", "value": "v",
803 "source_session": "s", "confidence": 0.8,
804 "created_at": "2024-01-01T00:00:00Z",
805 "last_confirmed": "2024-01-01T00:00:00Z"
806 }"#;
807 let fact: KnowledgeFact = serde_json::from_str(json).unwrap();
808 assert_eq!(fact.revision_count, 0);
809 }
810
811 #[test]
812 fn judged_pairs_default_empty_for_legacy() {
813 let json = r#"{
814 "project_root": "/test", "project_hash": "abc",
815 "facts": [], "patterns": [], "history": [],
816 "updated_at": "2024-01-01T00:00:00Z"
817 }"#;
818 let pk: ProjectKnowledge = serde_json::from_str(json).unwrap();
819 assert!(pk.judged_pairs.is_empty());
820 }
821
822 #[test]
823 fn cross_key_similar_finds_related_facts() {
824 let policy = default_policy();
825 let mut k = ProjectKnowledge::new("/tmp/test");
826 k.remember(
827 "architecture",
828 "auth",
829 "JWT RS256 token based authentication with Redis session store",
830 "s1",
831 0.9,
832 &policy,
833 );
834 k.remember(
835 "decision",
836 "session-model",
837 "JWT token authentication stored in Redis for session management",
838 "s1",
839 0.85,
840 &policy,
841 );
842 k.remember("deploy", "host", "AWS us-east-1", "s1", 0.8, &policy);
843
844 let similar = find_cross_key_similar(
845 "architecture",
846 "auth",
847 "JWT RS256 token based authentication with Redis session store",
848 &k.facts,
849 &k.judged_pairs,
850 3,
851 );
852 assert!(!similar.is_empty(), "should find session-model as similar");
853 assert_eq!(similar[0].category, "decision");
854 assert_eq!(similar[0].key, "session-model");
855 assert!(similar[0].similarity > 0.35);
856 }
857
858 #[test]
859 fn cross_key_similar_excludes_same_key() {
860 let policy = default_policy();
861 let mut k = ProjectKnowledge::new("/tmp/test");
862 k.remember("arch", "db", "PostgreSQL 16", "s1", 0.9, &policy);
863
864 let similar =
865 find_cross_key_similar("arch", "db", "PostgreSQL 16", &k.facts, &k.judged_pairs, 3);
866 assert!(similar.is_empty());
867 }
868
869 #[test]
870 fn cross_key_similar_excludes_judged_pairs() {
871 let policy = default_policy();
872 let mut k = ProjectKnowledge::new("/tmp/test");
873 k.remember(
874 "architecture",
875 "auth",
876 "JWT RS256 token based authentication with Redis",
877 "s1",
878 0.9,
879 &policy,
880 );
881 k.remember(
882 "decision",
883 "session-model",
884 "JWT token authentication stored in Redis",
885 "s1",
886 0.85,
887 &policy,
888 );
889
890 k.judged_pairs.push(JudgedPair {
891 key_a: "architecture/auth".into(),
892 key_b: "decision/session-model".into(),
893 verdict: "compatible".into(),
894 judged_at: Utc::now(),
895 });
896
897 let similar = find_cross_key_similar(
898 "architecture",
899 "auth",
900 "JWT RS256 token based authentication with Redis",
901 &k.facts,
902 &k.judged_pairs,
903 3,
904 );
905 assert!(similar.is_empty(), "judged pairs should be excluded");
906 }
907
908 #[test]
909 fn cross_key_similar_ignores_unrelated_facts() {
910 let policy = default_policy();
911 let mut k = ProjectKnowledge::new("/tmp/test");
912 k.remember(
913 "arch",
914 "db",
915 "PostgreSQL 16 with pgvector",
916 "s1",
917 0.9,
918 &policy,
919 );
920 k.remember("deploy", "host", "AWS us-east-1 region", "s1", 0.8, &policy);
921
922 let similar = find_cross_key_similar(
923 "arch",
924 "db",
925 "PostgreSQL 16 with pgvector",
926 &k.facts,
927 &k.judged_pairs,
928 3,
929 );
930 assert!(similar.is_empty(), "unrelated facts should not match");
931 }
932
933 #[test]
934 fn judge_supersedes_archives_target() {
935 let policy = default_policy();
936 let mut k = ProjectKnowledge::new("/tmp/test");
937 k.remember("architecture", "auth", "JWT RS256", "s1", 0.9, &policy);
938 k.remember("decision", "session", "JWT tokens", "s1", 0.85, &policy);
939
940 assert!(k.facts.iter().all(KnowledgeFact::is_current));
941
942 if let Some(tf) = k
943 .facts
944 .iter_mut()
945 .find(|f| f.category == "decision" && f.key == "session" && f.is_current())
946 {
947 tf.valid_until = Some(Utc::now());
948 }
949 k.judged_pairs.push(JudgedPair {
950 key_a: "architecture/auth".into(),
951 key_b: "decision/session".into(),
952 verdict: "supersedes".into(),
953 judged_at: Utc::now(),
954 });
955
956 let cur: Vec<_> = k.facts.iter().filter(|f| f.is_current()).collect();
957 assert_eq!(cur.len(), 1);
958 assert_eq!(cur[0].category, "architecture");
959 }
960}