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