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 .expect("consolidation succeeds");
467 assert_eq!(k.history.len(), 1);
468 assert_eq!(k.history[0].from_sessions.len(), 2);
469 }
470
471 #[test]
472 fn format_summary_output() {
473 let policy = default_policy();
474 let mut k = ProjectKnowledge::new("/tmp/test");
475 k.remember("architecture", "auth", "JWT RS256", "s1", 0.9, &policy);
476 k.add_pattern(
477 "naming",
478 "snake_case for functions",
479 vec!["get_user()".into()],
480 "s1",
481 &policy,
482 );
483 let summary = k.format_summary();
484 assert!(summary.contains("PROJECT KNOWLEDGE:"));
485 assert!(summary.contains("auth: JWT RS256"));
486 assert!(summary.contains("PROJECT PATTERNS:"));
487 }
488
489 #[test]
490 fn temporal_recall_at_time() {
491 let policy = default_policy();
492 let mut k = ProjectKnowledge::new("/tmp/test");
493 k.remember("arch", "db", "PostgreSQL", "s1", 0.95, &policy);
494 k.facts[0].confirmation_count = 3;
495
496 let before_change = Utc::now();
497 std::thread::sleep(std::time::Duration::from_millis(10));
498
499 k.remember("arch", "db", "MySQL", "s2", 0.9, &policy);
500
501 let results = k.recall_at_time("db", before_change);
502 assert_eq!(results.len(), 1);
503 assert_eq!(results[0].value, "PostgreSQL");
504
505 let results_now = k.recall_at_time("db", Utc::now());
506 assert_eq!(results_now.len(), 1);
507 assert_eq!(results_now[0].value, "MySQL");
508 }
509
510 #[test]
511 fn timeline_shows_history() {
512 let policy = default_policy();
513 let mut k = ProjectKnowledge::new("/tmp/test");
514 k.remember("arch", "db", "PostgreSQL", "s1", 0.95, &policy);
515 k.facts[0].confirmation_count = 3;
516 k.remember("arch", "db", "MySQL", "s2", 0.9, &policy);
517
518 let timeline = k.timeline("arch");
519 assert_eq!(timeline.len(), 2);
520 assert!(!timeline[0].is_current());
521 assert!(timeline[1].is_current());
522 }
523
524 #[test]
525 fn wakeup_format() {
526 let policy = default_policy();
527 let mut k = ProjectKnowledge::new("/tmp/test");
528 k.remember("arch", "auth", "JWT", "s1", 0.95, &policy);
529 k.remember("arch", "db", "PG", "s1", 0.8, &policy);
530
531 let wakeup = k.format_wakeup();
534 assert!(wakeup.contains("FACTS:"));
535 assert!(wakeup.contains("[arch]"));
536 assert!(wakeup.contains("auth=JWT"));
537 assert!(wakeup.contains("db=PG"));
538 }
539
540 #[test]
541 fn salience_prioritizes_decisions_over_findings_at_similar_confidence() {
542 let policy = default_policy();
543 let mut k = ProjectKnowledge::new("/tmp/test");
544 k.remember("finding", "f1", "some thing", "s1", 0.9, &policy);
545 k.remember("decision", "d1", "important", "s1", 0.85, &policy);
546
547 let wakeup = k.format_wakeup();
550 let d = wakeup.find("d1=important").expect("decision in wakeup");
551 let f = wakeup.find("f1=some thing").expect("finding in wakeup");
552 assert!(
553 d < f,
554 "expected decision before finding in wakeup: {wakeup}"
555 );
556 }
557
558 #[test]
559 fn low_confidence_contradiction() {
560 let policy = default_policy();
561 let mut k = ProjectKnowledge::new("/tmp/test");
562 k.remember("arch", "db", "PostgreSQL", "s1", 0.4, &policy);
563
564 let c = k.check_contradiction("arch", "db", "MySQL", &policy);
565 assert!(c.is_some());
566 assert_eq!(c.unwrap().severity, ContradictionSeverity::Low);
567 }
568
569 #[test]
570 fn no_contradiction_for_same_value() {
571 let policy = default_policy();
572 let mut k = ProjectKnowledge::new("/tmp/test");
573 k.remember("arch", "db", "PostgreSQL", "s1", 0.95, &policy);
574
575 let c = k.check_contradiction("arch", "db", "PostgreSQL", &policy);
576 assert!(c.is_none());
577 }
578
579 #[test]
580 fn no_contradiction_for_similar_values() {
581 let policy = default_policy();
582 let mut k = ProjectKnowledge::new("/tmp/test");
583 k.remember(
584 "arch",
585 "db",
586 "PostgreSQL 16 production database server",
587 "s1",
588 0.95,
589 &policy,
590 );
591
592 let c = k.check_contradiction(
593 "arch",
594 "db",
595 "PostgreSQL 16 production database server config",
596 &policy,
597 );
598 assert!(c.is_none());
599 }
600
601 #[test]
602 fn import_skip_existing() {
603 let policy = default_policy();
604 let mut k = ProjectKnowledge::new("/tmp/test");
605 k.remember("arch", "db", "PostgreSQL", "s1", 0.95, &policy);
606
607 let incoming = vec![KnowledgeFact {
608 category: "arch".into(),
609 key: "db".into(),
610 value: "MySQL".into(),
611 source_session: "import".into(),
612 confidence: 0.8,
613 created_at: Utc::now(),
614 last_confirmed: Utc::now(),
615 retrieval_count: 0,
616 last_retrieved: None,
617 valid_from: Some(Utc::now()),
618 valid_until: None,
619 supersedes: None,
620 confirmation_count: 1,
621 feedback_up: 0,
622 feedback_down: 0,
623 last_feedback: None,
624 privacy: FactPrivacy::default(),
625 sensitivity: crate::core::sensitivity::SensitivityLevel::default(),
626 imported_from: None,
627 archetype: KnowledgeArchetype::default(),
628 fidelity: None,
629 revision_count: 0,
630 }];
631
632 let result = k.import_facts(incoming, ImportMerge::SkipExisting, "imp-1", &policy);
633 assert_eq!(result.skipped, 1);
634 assert_eq!(result.added, 0);
635 assert_eq!(k.facts.iter().filter(|f| f.is_current()).count(), 1);
636 }
637
638 #[test]
639 fn import_replace_existing() {
640 let policy = default_policy();
641 let mut k = ProjectKnowledge::new("/tmp/test");
642 k.remember("arch", "db", "PostgreSQL", "s1", 0.95, &policy);
643
644 let incoming = vec![KnowledgeFact {
645 category: "arch".into(),
646 key: "db".into(),
647 value: "MySQL".into(),
648 source_session: "import".into(),
649 confidence: 0.8,
650 created_at: Utc::now(),
651 last_confirmed: Utc::now(),
652 retrieval_count: 0,
653 last_retrieved: None,
654 valid_from: Some(Utc::now()),
655 valid_until: None,
656 supersedes: None,
657 confirmation_count: 1,
658 feedback_up: 0,
659 feedback_down: 0,
660 last_feedback: None,
661 privacy: FactPrivacy::default(),
662 sensitivity: crate::core::sensitivity::SensitivityLevel::default(),
663 imported_from: None,
664 archetype: KnowledgeArchetype::default(),
665 fidelity: None,
666 revision_count: 0,
667 }];
668
669 let result = k.import_facts(incoming, ImportMerge::Replace, "imp-1", &policy);
670 assert_eq!(result.replaced, 1);
671 let current: Vec<_> = k.facts.iter().filter(|f| f.is_current()).collect();
672 assert_eq!(current.len(), 1);
673 assert_eq!(current[0].value, "MySQL");
674 }
675
676 #[test]
677 fn import_adds_new_facts() {
678 let policy = default_policy();
679 let mut k = ProjectKnowledge::new("/tmp/test");
680 k.remember("arch", "db", "PostgreSQL", "s1", 0.95, &policy);
681
682 let incoming = vec![KnowledgeFact {
683 category: "security".into(),
684 key: "auth".into(),
685 value: "JWT".into(),
686 source_session: "import".into(),
687 confidence: 0.9,
688 created_at: Utc::now(),
689 last_confirmed: Utc::now(),
690 retrieval_count: 0,
691 last_retrieved: None,
692 valid_from: Some(Utc::now()),
693 valid_until: None,
694 supersedes: None,
695 confirmation_count: 1,
696 feedback_up: 0,
697 feedback_down: 0,
698 last_feedback: None,
699 privacy: FactPrivacy::default(),
700 sensitivity: crate::core::sensitivity::SensitivityLevel::default(),
701 imported_from: None,
702 archetype: KnowledgeArchetype::default(),
703 fidelity: None,
704 revision_count: 0,
705 }];
706
707 let result = k.import_facts(incoming, ImportMerge::SkipExisting, "imp-1", &policy);
708 assert_eq!(result.added, 1);
709 assert_eq!(k.facts.iter().filter(|f| f.is_current()).count(), 2);
710 }
711
712 #[test]
713 fn parse_simple_json_array() {
714 let data = r#"[
715 {"category": "arch", "key": "db", "value": "PostgreSQL"},
716 {"category": "security", "key": "auth", "value": "JWT", "confidence": 0.9}
717 ]"#;
718 let facts = parse_import_data(data).unwrap();
719 assert_eq!(facts.len(), 2);
720 assert_eq!(facts[0].category, "arch");
721 assert_eq!(facts[1].confidence, 0.9);
722 }
723
724 #[test]
725 fn parse_jsonl_format() {
726 let data = "{\"category\":\"arch\",\"key\":\"db\",\"value\":\"PG\"}\n\
727 {\"category\":\"security\",\"key\":\"auth\",\"value\":\"JWT\"}";
728 let facts = parse_import_data(data).unwrap();
729 assert_eq!(facts.len(), 2);
730 }
731
732 #[test]
733 fn export_simple_only_current() {
734 let policy = default_policy();
735 let mut k = ProjectKnowledge::new("/tmp/test");
736 k.remember("arch", "db", "PostgreSQL", "s1", 0.95, &policy);
737 k.remember("arch", "db", "MySQL", "s2", 0.9, &policy);
738
739 let exported = k.export_simple();
740 assert_eq!(exported.len(), 1);
741 assert_eq!(exported[0].value, "MySQL");
742 }
743
744 #[test]
745 fn import_merge_parse() {
746 assert_eq!(ImportMerge::parse("replace"), Some(ImportMerge::Replace));
747 assert_eq!(ImportMerge::parse("append"), Some(ImportMerge::Append));
748 assert_eq!(
749 ImportMerge::parse("skip-existing"),
750 Some(ImportMerge::SkipExisting)
751 );
752 assert_eq!(
753 ImportMerge::parse("skip_existing"),
754 Some(ImportMerge::SkipExisting)
755 );
756 assert_eq!(ImportMerge::parse("skip"), Some(ImportMerge::SkipExisting));
757 assert!(ImportMerge::parse("invalid").is_none());
758 }
759
760 #[test]
761 fn revision_count_on_new_fact() {
762 let policy = default_policy();
763 let mut k = ProjectKnowledge::new("/tmp/test");
764 k.remember("arch", "db", "PostgreSQL", "s1", 0.9, &policy);
765 let cur = k.facts.iter().find(|f| f.is_current()).unwrap();
766 assert_eq!(cur.revision_count, 1);
767 }
768
769 #[test]
770 fn revision_count_increments_on_confirm() {
771 let policy = default_policy();
772 let mut k = ProjectKnowledge::new("/tmp/test");
773 k.remember("arch", "db", "PostgreSQL", "s1", 0.9, &policy);
774 k.remember("arch", "db", "PostgreSQL", "s2", 0.9, &policy);
775 k.remember("arch", "db", "PostgreSQL", "s3", 0.9, &policy);
776 let cur = k.facts.iter().find(|f| f.is_current()).unwrap();
777 assert_eq!(cur.revision_count, 3);
778 assert_eq!(cur.confirmation_count, 3);
779 }
780
781 #[test]
782 fn revision_count_carries_over_on_supersede() {
783 let policy = default_policy();
784 let mut k = ProjectKnowledge::new("/tmp/test");
785 k.remember("arch", "db", "PostgreSQL", "s1", 0.95, &policy);
786 k.remember("arch", "db", "PostgreSQL", "s2", 0.9, &policy);
787 assert_eq!(
788 k.facts
789 .iter()
790 .find(|f| f.is_current())
791 .unwrap()
792 .revision_count,
793 2
794 );
795 k.facts[0].confirmation_count = 3;
796 k.remember("arch", "db", "MySQL", "s3", 0.9, &policy);
797 let cur: Vec<_> = k.facts.iter().filter(|f| f.is_current()).collect();
798 assert_eq!(cur.len(), 1);
799 assert_eq!(cur[0].value, "MySQL");
800 assert_eq!(cur[0].revision_count, 3);
801 assert!(cur[0].supersedes.is_some());
802 }
803
804 #[test]
805 fn revision_count_default_zero_for_legacy() {
806 let json = r#"{
807 "category": "test", "key": "k", "value": "v",
808 "source_session": "s", "confidence": 0.8,
809 "created_at": "2024-01-01T00:00:00Z",
810 "last_confirmed": "2024-01-01T00:00:00Z"
811 }"#;
812 let fact: KnowledgeFact = serde_json::from_str(json).unwrap();
813 assert_eq!(fact.revision_count, 0);
814 }
815
816 #[test]
817 fn judged_pairs_default_empty_for_legacy() {
818 let json = r#"{
819 "project_root": "/test", "project_hash": "abc",
820 "facts": [], "patterns": [], "history": [],
821 "updated_at": "2024-01-01T00:00:00Z"
822 }"#;
823 let pk: ProjectKnowledge = serde_json::from_str(json).unwrap();
824 assert!(pk.judged_pairs.is_empty());
825 }
826
827 #[test]
828 fn cross_key_similar_finds_related_facts() {
829 let policy = default_policy();
830 let mut k = ProjectKnowledge::new("/tmp/test");
831 k.remember(
832 "architecture",
833 "auth",
834 "JWT RS256 token based authentication with Redis session store",
835 "s1",
836 0.9,
837 &policy,
838 );
839 k.remember(
840 "decision",
841 "session-model",
842 "JWT token authentication stored in Redis for session management",
843 "s1",
844 0.85,
845 &policy,
846 );
847 k.remember("deploy", "host", "AWS us-east-1", "s1", 0.8, &policy);
848
849 let similar = find_cross_key_similar(
850 "architecture",
851 "auth",
852 "JWT RS256 token based authentication with Redis session store",
853 &k.facts,
854 &k.judged_pairs,
855 3,
856 );
857 assert!(!similar.is_empty(), "should find session-model as similar");
858 assert_eq!(similar[0].category, "decision");
859 assert_eq!(similar[0].key, "session-model");
860 assert!(similar[0].similarity > 0.35);
861 }
862
863 #[test]
864 fn cross_key_similar_excludes_same_key() {
865 let policy = default_policy();
866 let mut k = ProjectKnowledge::new("/tmp/test");
867 k.remember("arch", "db", "PostgreSQL 16", "s1", 0.9, &policy);
868
869 let similar =
870 find_cross_key_similar("arch", "db", "PostgreSQL 16", &k.facts, &k.judged_pairs, 3);
871 assert!(similar.is_empty());
872 }
873
874 #[test]
875 fn cross_key_similar_excludes_judged_pairs() {
876 let policy = default_policy();
877 let mut k = ProjectKnowledge::new("/tmp/test");
878 k.remember(
879 "architecture",
880 "auth",
881 "JWT RS256 token based authentication with Redis",
882 "s1",
883 0.9,
884 &policy,
885 );
886 k.remember(
887 "decision",
888 "session-model",
889 "JWT token authentication stored in Redis",
890 "s1",
891 0.85,
892 &policy,
893 );
894
895 k.judged_pairs.push(JudgedPair {
896 key_a: "architecture/auth".into(),
897 key_b: "decision/session-model".into(),
898 verdict: "compatible".into(),
899 judged_at: Utc::now(),
900 });
901
902 let similar = find_cross_key_similar(
903 "architecture",
904 "auth",
905 "JWT RS256 token based authentication with Redis",
906 &k.facts,
907 &k.judged_pairs,
908 3,
909 );
910 assert!(similar.is_empty(), "judged pairs should be excluded");
911 }
912
913 #[test]
914 fn cross_key_similar_ignores_unrelated_facts() {
915 let policy = default_policy();
916 let mut k = ProjectKnowledge::new("/tmp/test");
917 k.remember(
918 "arch",
919 "db",
920 "PostgreSQL 16 with pgvector",
921 "s1",
922 0.9,
923 &policy,
924 );
925 k.remember("deploy", "host", "AWS us-east-1 region", "s1", 0.8, &policy);
926
927 let similar = find_cross_key_similar(
928 "arch",
929 "db",
930 "PostgreSQL 16 with pgvector",
931 &k.facts,
932 &k.judged_pairs,
933 3,
934 );
935 assert!(similar.is_empty(), "unrelated facts should not match");
936 }
937
938 #[test]
939 fn judge_supersedes_archives_target() {
940 let policy = default_policy();
941 let mut k = ProjectKnowledge::new("/tmp/test");
942 k.remember("architecture", "auth", "JWT RS256", "s1", 0.9, &policy);
943 k.remember("decision", "session", "JWT tokens", "s1", 0.85, &policy);
944
945 assert!(k.facts.iter().all(KnowledgeFact::is_current));
946
947 if let Some(tf) = k
948 .facts
949 .iter_mut()
950 .find(|f| f.category == "decision" && f.key == "session" && f.is_current())
951 {
952 tf.valid_until = Some(Utc::now());
953 }
954 k.judged_pairs.push(JudgedPair {
955 key_a: "architecture/auth".into(),
956 key_b: "decision/session".into(),
957 verdict: "supersedes".into(),
958 judged_at: Utc::now(),
959 });
960
961 let cur: Vec<_> = k.facts.iter().filter(|f| f.is_current()).collect();
962 assert_eq!(cur.len(), 1);
963 assert_eq!(cur[0].category, "architecture");
964 }
965}