Skip to main content

lean_ctx/core/knowledge/
mod.rs

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 facts_evict_down_to_cap_not_double() {
52        // Regression: remember() must keep the fact count at or below max_facts.
53        // Previously the lifecycle only fired above 2 * max_facts, so a store
54        // could silently grow to twice its configured budget before reclaiming.
55        let mut policy = default_policy();
56        policy.knowledge.max_facts = 5;
57        let mut k = ProjectKnowledge::new("/tmp/test-evict");
58        for i in 0..40 {
59            k.remember(
60                "finding",
61                &format!("key-{i}"),
62                &format!("value number {i}"),
63                "s1",
64                0.7,
65                &policy,
66            );
67        }
68        assert!(
69            k.facts.len() <= policy.knowledge.max_facts,
70            "expected <= {} facts after eviction, got {}",
71            policy.knowledge.max_facts,
72            k.facts.len()
73        );
74    }
75
76    #[test]
77    fn import_facts_evict_down_to_cap_not_double() {
78        // Regression: import_facts() must settle the store at <= max_facts, just
79        // like remember(). The import path previously only ran the lifecycle above
80        // 2 * max_facts, so a bulk import could inflate a store to twice its
81        // budget (observed live as doctor CRIT facts 232/200) while remember()
82        // already capped at max_facts.
83        let mut policy = default_policy();
84        policy.knowledge.max_facts = 5;
85
86        // Produce a batch larger than 2x the cap via a generously-capped source.
87        let mut source_policy = default_policy();
88        source_policy.knowledge.max_facts = 1000;
89        let mut source = ProjectKnowledge::new("/tmp/test-import-source");
90        for i in 0..40 {
91            source.remember(
92                "finding",
93                &format!("key-{i}"),
94                &format!("value number {i}"),
95                "s1",
96                0.7,
97                &source_policy,
98            );
99        }
100        let incoming = source.facts.clone();
101        assert!(
102            incoming.len() > policy.knowledge.max_facts * 2,
103            "test needs a batch larger than 2x cap to exercise the old guard, got {}",
104            incoming.len()
105        );
106
107        let mut k = ProjectKnowledge::new("/tmp/test-import-target");
108        k.import_facts(incoming, ImportMerge::Append, "s2", &policy);
109        assert!(
110            k.facts.len() <= policy.knowledge.max_facts,
111            "expected <= {} facts after import eviction, got {}",
112            policy.knowledge.max_facts,
113            k.facts.len()
114        );
115    }
116
117    #[test]
118    fn upsert_existing_fact() {
119        let policy = default_policy();
120        let mut k = ProjectKnowledge::new("/tmp/test");
121        k.remember("arch", "db", "PostgreSQL", "s1", 0.7, &policy);
122        k.remember(
123            "arch",
124            "db",
125            "PostgreSQL 16 with pgvector",
126            "s2",
127            0.95,
128            &policy,
129        );
130
131        let current: Vec<_> = k.facts.iter().filter(|f| f.is_current()).collect();
132        assert_eq!(current.len(), 1);
133        assert_eq!(current[0].value, "PostgreSQL 16 with pgvector");
134    }
135
136    #[test]
137    fn contradiction_detection() {
138        let policy = default_policy();
139        let mut k = ProjectKnowledge::new("/tmp/test");
140        k.remember("arch", "db", "PostgreSQL", "s1", 0.95, &policy);
141        k.facts[0].confirmation_count = 3;
142
143        let contradiction = k.check_contradiction("arch", "db", "MySQL", &policy);
144        assert!(contradiction.is_some());
145        let c = contradiction.unwrap();
146        assert_eq!(c.severity, ContradictionSeverity::High);
147    }
148
149    #[test]
150    fn temporal_validity() {
151        let policy = default_policy();
152        let mut k = ProjectKnowledge::new("/tmp/test");
153        k.remember("arch", "db", "PostgreSQL", "s1", 0.95, &policy);
154        k.facts[0].confirmation_count = 3;
155
156        k.remember("arch", "db", "MySQL", "s2", 0.9, &policy);
157
158        let current: Vec<_> = k.facts.iter().filter(|f| f.is_current()).collect();
159        assert_eq!(current.len(), 1);
160        assert_eq!(current[0].value, "MySQL");
161
162        let all_db: Vec<_> = k.facts.iter().filter(|f| f.key == "db").collect();
163        assert_eq!(all_db.len(), 2);
164    }
165
166    #[test]
167    fn confirmation_count() {
168        let policy = default_policy();
169        let mut k = ProjectKnowledge::new("/tmp/test");
170        k.remember("arch", "db", "PostgreSQL", "s1", 0.9, &policy);
171        assert_eq!(k.facts[0].confirmation_count, 1);
172
173        k.remember("arch", "db", "PostgreSQL", "s2", 0.9, &policy);
174        assert_eq!(k.facts[0].confirmation_count, 2);
175    }
176
177    #[test]
178    fn remove_fact() {
179        let policy = default_policy();
180        let mut k = ProjectKnowledge::new("/tmp/test");
181        k.remember("arch", "db", "PostgreSQL", "s1", 0.9, &policy);
182        assert!(k.remove_fact("arch", "db"));
183        assert!(k.facts.is_empty());
184        assert!(!k.remove_fact("arch", "db"));
185    }
186
187    #[test]
188    fn list_rooms() {
189        let policy = default_policy();
190        let mut k = ProjectKnowledge::new("/tmp/test");
191        k.remember("architecture", "auth", "JWT", "s1", 0.9, &policy);
192        k.remember("architecture", "db", "PG", "s1", 0.9, &policy);
193        k.remember("deploy", "host", "AWS", "s1", 0.8, &policy);
194
195        let rooms = k.list_rooms();
196        assert_eq!(rooms.len(), 2);
197    }
198
199    #[test]
200    fn aaak_format() {
201        let policy = default_policy();
202        let mut k = ProjectKnowledge::new("/tmp/test");
203        k.remember("architecture", "auth", "JWT RS256", "s1", 0.95, &policy);
204        k.remember("architecture", "db", "PostgreSQL", "s1", 0.7, &policy);
205
206        let aaak = k.format_aaak();
207        assert!(aaak.contains("ARCHITECTURE:"));
208        assert!(aaak.contains("auth=JWT RS256"));
209    }
210
211    #[test]
212    fn consolidate_history() {
213        let policy = default_policy();
214        let mut k = ProjectKnowledge::new("/tmp/test");
215        k.consolidate(
216            "Migrated from REST to GraphQL",
217            vec!["s1".into(), "s2".into()],
218            &policy,
219        );
220        assert_eq!(k.history.len(), 1);
221        assert_eq!(k.history[0].from_sessions.len(), 2);
222    }
223
224    #[test]
225    fn format_summary_output() {
226        let policy = default_policy();
227        let mut k = ProjectKnowledge::new("/tmp/test");
228        k.remember("architecture", "auth", "JWT RS256", "s1", 0.9, &policy);
229        k.add_pattern(
230            "naming",
231            "snake_case for functions",
232            vec!["get_user()".into()],
233            "s1",
234            &policy,
235        );
236        let summary = k.format_summary();
237        assert!(summary.contains("PROJECT KNOWLEDGE:"));
238        assert!(summary.contains("auth: JWT RS256"));
239        assert!(summary.contains("PROJECT PATTERNS:"));
240    }
241
242    #[test]
243    fn temporal_recall_at_time() {
244        let policy = default_policy();
245        let mut k = ProjectKnowledge::new("/tmp/test");
246        k.remember("arch", "db", "PostgreSQL", "s1", 0.95, &policy);
247        k.facts[0].confirmation_count = 3;
248
249        let before_change = Utc::now();
250        std::thread::sleep(std::time::Duration::from_millis(10));
251
252        k.remember("arch", "db", "MySQL", "s2", 0.9, &policy);
253
254        let results = k.recall_at_time("db", before_change);
255        assert_eq!(results.len(), 1);
256        assert_eq!(results[0].value, "PostgreSQL");
257
258        let results_now = k.recall_at_time("db", Utc::now());
259        assert_eq!(results_now.len(), 1);
260        assert_eq!(results_now[0].value, "MySQL");
261    }
262
263    #[test]
264    fn timeline_shows_history() {
265        let policy = default_policy();
266        let mut k = ProjectKnowledge::new("/tmp/test");
267        k.remember("arch", "db", "PostgreSQL", "s1", 0.95, &policy);
268        k.facts[0].confirmation_count = 3;
269        k.remember("arch", "db", "MySQL", "s2", 0.9, &policy);
270
271        let timeline = k.timeline("arch");
272        assert_eq!(timeline.len(), 2);
273        assert!(!timeline[0].is_current());
274        assert!(timeline[1].is_current());
275    }
276
277    #[test]
278    fn wakeup_format() {
279        let policy = default_policy();
280        let mut k = ProjectKnowledge::new("/tmp/test");
281        k.remember("arch", "auth", "JWT", "s1", 0.95, &policy);
282        k.remember("arch", "db", "PG", "s1", 0.8, &policy);
283
284        // Theta-gamma chunked format (#543): same-category facts share one
285        // `[topic]` header that amortizes the category prefix.
286        let wakeup = k.format_wakeup();
287        assert!(wakeup.contains("FACTS:"));
288        assert!(wakeup.contains("[arch]"));
289        assert!(wakeup.contains("auth=JWT"));
290        assert!(wakeup.contains("db=PG"));
291    }
292
293    #[test]
294    fn salience_prioritizes_decisions_over_findings_at_similar_confidence() {
295        let policy = default_policy();
296        let mut k = ProjectKnowledge::new("/tmp/test");
297        k.remember("finding", "f1", "some thing", "s1", 0.9, &policy);
298        k.remember("decision", "d1", "important", "s1", 0.85, &policy);
299
300        // Chunked format (#543): the founding fact of the first chunk is the
301        // most salient one, so the decision chunk must render first.
302        let wakeup = k.format_wakeup();
303        let d = wakeup.find("d1=important").expect("decision in wakeup");
304        let f = wakeup.find("f1=some thing").expect("finding in wakeup");
305        assert!(
306            d < f,
307            "expected decision before finding in wakeup: {wakeup}"
308        );
309    }
310
311    #[test]
312    fn low_confidence_contradiction() {
313        let policy = default_policy();
314        let mut k = ProjectKnowledge::new("/tmp/test");
315        k.remember("arch", "db", "PostgreSQL", "s1", 0.4, &policy);
316
317        let c = k.check_contradiction("arch", "db", "MySQL", &policy);
318        assert!(c.is_some());
319        assert_eq!(c.unwrap().severity, ContradictionSeverity::Low);
320    }
321
322    #[test]
323    fn no_contradiction_for_same_value() {
324        let policy = default_policy();
325        let mut k = ProjectKnowledge::new("/tmp/test");
326        k.remember("arch", "db", "PostgreSQL", "s1", 0.95, &policy);
327
328        let c = k.check_contradiction("arch", "db", "PostgreSQL", &policy);
329        assert!(c.is_none());
330    }
331
332    #[test]
333    fn no_contradiction_for_similar_values() {
334        let policy = default_policy();
335        let mut k = ProjectKnowledge::new("/tmp/test");
336        k.remember(
337            "arch",
338            "db",
339            "PostgreSQL 16 production database server",
340            "s1",
341            0.95,
342            &policy,
343        );
344
345        let c = k.check_contradiction(
346            "arch",
347            "db",
348            "PostgreSQL 16 production database server config",
349            &policy,
350        );
351        assert!(c.is_none());
352    }
353
354    #[test]
355    fn import_skip_existing() {
356        let policy = default_policy();
357        let mut k = ProjectKnowledge::new("/tmp/test");
358        k.remember("arch", "db", "PostgreSQL", "s1", 0.95, &policy);
359
360        let incoming = vec![KnowledgeFact {
361            category: "arch".into(),
362            key: "db".into(),
363            value: "MySQL".into(),
364            source_session: "import".into(),
365            confidence: 0.8,
366            created_at: Utc::now(),
367            last_confirmed: Utc::now(),
368            retrieval_count: 0,
369            last_retrieved: None,
370            valid_from: Some(Utc::now()),
371            valid_until: None,
372            supersedes: None,
373            confirmation_count: 1,
374            feedback_up: 0,
375            feedback_down: 0,
376            last_feedback: None,
377            privacy: FactPrivacy::default(),
378            sensitivity: crate::core::sensitivity::SensitivityLevel::default(),
379            imported_from: None,
380            archetype: KnowledgeArchetype::default(),
381            fidelity: None,
382            revision_count: 0,
383        }];
384
385        let result = k.import_facts(incoming, ImportMerge::SkipExisting, "imp-1", &policy);
386        assert_eq!(result.skipped, 1);
387        assert_eq!(result.added, 0);
388        assert_eq!(k.facts.iter().filter(|f| f.is_current()).count(), 1);
389    }
390
391    #[test]
392    fn import_replace_existing() {
393        let policy = default_policy();
394        let mut k = ProjectKnowledge::new("/tmp/test");
395        k.remember("arch", "db", "PostgreSQL", "s1", 0.95, &policy);
396
397        let incoming = vec![KnowledgeFact {
398            category: "arch".into(),
399            key: "db".into(),
400            value: "MySQL".into(),
401            source_session: "import".into(),
402            confidence: 0.8,
403            created_at: Utc::now(),
404            last_confirmed: Utc::now(),
405            retrieval_count: 0,
406            last_retrieved: None,
407            valid_from: Some(Utc::now()),
408            valid_until: None,
409            supersedes: None,
410            confirmation_count: 1,
411            feedback_up: 0,
412            feedback_down: 0,
413            last_feedback: None,
414            privacy: FactPrivacy::default(),
415            sensitivity: crate::core::sensitivity::SensitivityLevel::default(),
416            imported_from: None,
417            archetype: KnowledgeArchetype::default(),
418            fidelity: None,
419            revision_count: 0,
420        }];
421
422        let result = k.import_facts(incoming, ImportMerge::Replace, "imp-1", &policy);
423        assert_eq!(result.replaced, 1);
424        let current: Vec<_> = k.facts.iter().filter(|f| f.is_current()).collect();
425        assert_eq!(current.len(), 1);
426        assert_eq!(current[0].value, "MySQL");
427    }
428
429    #[test]
430    fn import_adds_new_facts() {
431        let policy = default_policy();
432        let mut k = ProjectKnowledge::new("/tmp/test");
433        k.remember("arch", "db", "PostgreSQL", "s1", 0.95, &policy);
434
435        let incoming = vec![KnowledgeFact {
436            category: "security".into(),
437            key: "auth".into(),
438            value: "JWT".into(),
439            source_session: "import".into(),
440            confidence: 0.9,
441            created_at: Utc::now(),
442            last_confirmed: Utc::now(),
443            retrieval_count: 0,
444            last_retrieved: None,
445            valid_from: Some(Utc::now()),
446            valid_until: None,
447            supersedes: None,
448            confirmation_count: 1,
449            feedback_up: 0,
450            feedback_down: 0,
451            last_feedback: None,
452            privacy: FactPrivacy::default(),
453            sensitivity: crate::core::sensitivity::SensitivityLevel::default(),
454            imported_from: None,
455            archetype: KnowledgeArchetype::default(),
456            fidelity: None,
457            revision_count: 0,
458        }];
459
460        let result = k.import_facts(incoming, ImportMerge::SkipExisting, "imp-1", &policy);
461        assert_eq!(result.added, 1);
462        assert_eq!(k.facts.iter().filter(|f| f.is_current()).count(), 2);
463    }
464
465    #[test]
466    fn parse_simple_json_array() {
467        let data = r#"[
468            {"category": "arch", "key": "db", "value": "PostgreSQL"},
469            {"category": "security", "key": "auth", "value": "JWT", "confidence": 0.9}
470        ]"#;
471        let facts = parse_import_data(data).unwrap();
472        assert_eq!(facts.len(), 2);
473        assert_eq!(facts[0].category, "arch");
474        assert_eq!(facts[1].confidence, 0.9);
475    }
476
477    #[test]
478    fn parse_jsonl_format() {
479        let data = "{\"category\":\"arch\",\"key\":\"db\",\"value\":\"PG\"}\n\
480                    {\"category\":\"security\",\"key\":\"auth\",\"value\":\"JWT\"}";
481        let facts = parse_import_data(data).unwrap();
482        assert_eq!(facts.len(), 2);
483    }
484
485    #[test]
486    fn export_simple_only_current() {
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.remember("arch", "db", "MySQL", "s2", 0.9, &policy);
491
492        let exported = k.export_simple();
493        assert_eq!(exported.len(), 1);
494        assert_eq!(exported[0].value, "MySQL");
495    }
496
497    #[test]
498    fn import_merge_parse() {
499        assert_eq!(ImportMerge::parse("replace"), Some(ImportMerge::Replace));
500        assert_eq!(ImportMerge::parse("append"), Some(ImportMerge::Append));
501        assert_eq!(
502            ImportMerge::parse("skip-existing"),
503            Some(ImportMerge::SkipExisting)
504        );
505        assert_eq!(
506            ImportMerge::parse("skip_existing"),
507            Some(ImportMerge::SkipExisting)
508        );
509        assert_eq!(ImportMerge::parse("skip"), Some(ImportMerge::SkipExisting));
510        assert!(ImportMerge::parse("invalid").is_none());
511    }
512
513    #[test]
514    fn revision_count_on_new_fact() {
515        let policy = default_policy();
516        let mut k = ProjectKnowledge::new("/tmp/test");
517        k.remember("arch", "db", "PostgreSQL", "s1", 0.9, &policy);
518        let cur = k.facts.iter().find(|f| f.is_current()).unwrap();
519        assert_eq!(cur.revision_count, 1);
520    }
521
522    #[test]
523    fn revision_count_increments_on_confirm() {
524        let policy = default_policy();
525        let mut k = ProjectKnowledge::new("/tmp/test");
526        k.remember("arch", "db", "PostgreSQL", "s1", 0.9, &policy);
527        k.remember("arch", "db", "PostgreSQL", "s2", 0.9, &policy);
528        k.remember("arch", "db", "PostgreSQL", "s3", 0.9, &policy);
529        let cur = k.facts.iter().find(|f| f.is_current()).unwrap();
530        assert_eq!(cur.revision_count, 3);
531        assert_eq!(cur.confirmation_count, 3);
532    }
533
534    #[test]
535    fn revision_count_carries_over_on_supersede() {
536        let policy = default_policy();
537        let mut k = ProjectKnowledge::new("/tmp/test");
538        k.remember("arch", "db", "PostgreSQL", "s1", 0.95, &policy);
539        k.remember("arch", "db", "PostgreSQL", "s2", 0.9, &policy);
540        assert_eq!(
541            k.facts
542                .iter()
543                .find(|f| f.is_current())
544                .unwrap()
545                .revision_count,
546            2
547        );
548        k.facts[0].confirmation_count = 3;
549        k.remember("arch", "db", "MySQL", "s3", 0.9, &policy);
550        let cur: Vec<_> = k.facts.iter().filter(|f| f.is_current()).collect();
551        assert_eq!(cur.len(), 1);
552        assert_eq!(cur[0].value, "MySQL");
553        assert_eq!(cur[0].revision_count, 3);
554        assert!(cur[0].supersedes.is_some());
555    }
556
557    #[test]
558    fn revision_count_default_zero_for_legacy() {
559        let json = r#"{
560            "category": "test", "key": "k", "value": "v",
561            "source_session": "s", "confidence": 0.8,
562            "created_at": "2024-01-01T00:00:00Z",
563            "last_confirmed": "2024-01-01T00:00:00Z"
564        }"#;
565        let fact: KnowledgeFact = serde_json::from_str(json).unwrap();
566        assert_eq!(fact.revision_count, 0);
567    }
568
569    #[test]
570    fn judged_pairs_default_empty_for_legacy() {
571        let json = r#"{
572            "project_root": "/test", "project_hash": "abc",
573            "facts": [], "patterns": [], "history": [],
574            "updated_at": "2024-01-01T00:00:00Z"
575        }"#;
576        let pk: ProjectKnowledge = serde_json::from_str(json).unwrap();
577        assert!(pk.judged_pairs.is_empty());
578    }
579
580    #[test]
581    fn cross_key_similar_finds_related_facts() {
582        let policy = default_policy();
583        let mut k = ProjectKnowledge::new("/tmp/test");
584        k.remember(
585            "architecture",
586            "auth",
587            "JWT RS256 token based authentication with Redis session store",
588            "s1",
589            0.9,
590            &policy,
591        );
592        k.remember(
593            "decision",
594            "session-model",
595            "JWT token authentication stored in Redis for session management",
596            "s1",
597            0.85,
598            &policy,
599        );
600        k.remember("deploy", "host", "AWS us-east-1", "s1", 0.8, &policy);
601
602        let similar = find_cross_key_similar(
603            "architecture",
604            "auth",
605            "JWT RS256 token based authentication with Redis session store",
606            &k.facts,
607            &k.judged_pairs,
608            3,
609        );
610        assert!(!similar.is_empty(), "should find session-model as similar");
611        assert_eq!(similar[0].category, "decision");
612        assert_eq!(similar[0].key, "session-model");
613        assert!(similar[0].similarity > 0.35);
614    }
615
616    #[test]
617    fn cross_key_similar_excludes_same_key() {
618        let policy = default_policy();
619        let mut k = ProjectKnowledge::new("/tmp/test");
620        k.remember("arch", "db", "PostgreSQL 16", "s1", 0.9, &policy);
621
622        let similar =
623            find_cross_key_similar("arch", "db", "PostgreSQL 16", &k.facts, &k.judged_pairs, 3);
624        assert!(similar.is_empty());
625    }
626
627    #[test]
628    fn cross_key_similar_excludes_judged_pairs() {
629        let policy = default_policy();
630        let mut k = ProjectKnowledge::new("/tmp/test");
631        k.remember(
632            "architecture",
633            "auth",
634            "JWT RS256 token based authentication with Redis",
635            "s1",
636            0.9,
637            &policy,
638        );
639        k.remember(
640            "decision",
641            "session-model",
642            "JWT token authentication stored in Redis",
643            "s1",
644            0.85,
645            &policy,
646        );
647
648        k.judged_pairs.push(JudgedPair {
649            key_a: "architecture/auth".into(),
650            key_b: "decision/session-model".into(),
651            verdict: "compatible".into(),
652            judged_at: Utc::now(),
653        });
654
655        let similar = find_cross_key_similar(
656            "architecture",
657            "auth",
658            "JWT RS256 token based authentication with Redis",
659            &k.facts,
660            &k.judged_pairs,
661            3,
662        );
663        assert!(similar.is_empty(), "judged pairs should be excluded");
664    }
665
666    #[test]
667    fn cross_key_similar_ignores_unrelated_facts() {
668        let policy = default_policy();
669        let mut k = ProjectKnowledge::new("/tmp/test");
670        k.remember(
671            "arch",
672            "db",
673            "PostgreSQL 16 with pgvector",
674            "s1",
675            0.9,
676            &policy,
677        );
678        k.remember("deploy", "host", "AWS us-east-1 region", "s1", 0.8, &policy);
679
680        let similar = find_cross_key_similar(
681            "arch",
682            "db",
683            "PostgreSQL 16 with pgvector",
684            &k.facts,
685            &k.judged_pairs,
686            3,
687        );
688        assert!(similar.is_empty(), "unrelated facts should not match");
689    }
690
691    #[test]
692    fn judge_supersedes_archives_target() {
693        let policy = default_policy();
694        let mut k = ProjectKnowledge::new("/tmp/test");
695        k.remember("architecture", "auth", "JWT RS256", "s1", 0.9, &policy);
696        k.remember("decision", "session", "JWT tokens", "s1", 0.85, &policy);
697
698        assert!(k.facts.iter().all(KnowledgeFact::is_current));
699
700        if let Some(tf) = k
701            .facts
702            .iter_mut()
703            .find(|f| f.category == "decision" && f.key == "session" && f.is_current())
704        {
705            tf.valid_until = Some(Utc::now());
706        }
707        k.judged_pairs.push(JudgedPair {
708            key_a: "architecture/auth".into(),
709            key_b: "decision/session".into(),
710            verdict: "supersedes".into(),
711            judged_at: Utc::now(),
712        });
713
714        let cur: Vec<_> = k.facts.iter().filter(|f| f.is_current()).collect();
715        assert_eq!(cur.len(), 1);
716        assert_eq!(cur[0].category, "architecture");
717    }
718}