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