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