1pub mod agents_md;
2pub mod anchors;
3pub mod chunking;
4mod core;
5mod fact;
6mod format;
7pub mod hooks;
8mod import_export;
9pub mod import_tag;
10pub mod journal;
11pub mod local_store;
12pub mod maintenance;
13pub mod okf;
14mod persist;
15pub mod protection;
16mod query;
17mod ranking;
18pub mod retrieval;
19pub mod snapshot;
20pub mod source_view;
21pub mod store;
22pub mod supersession;
23mod types;
24
25pub use format::fact_served;
26pub use import_export::{ImportMerge, ImportResult, SimpleFactEntry, parse_import_data};
27pub use persist::is_machine_derived;
28pub use persist::knowledge_dir_of;
29pub use query::KnowledgeQuery;
30pub use ranking::fact_version_id_v1;
31pub use ranking::sort_fact_for_output;
32pub use ranking::{SimilarFact, find_cross_key_similar, string_similarity};
33pub use snapshot::KnowledgeSnapshot;
34pub use types::*;
35
36#[cfg(test)]
37mod tests {
38 use super::*;
39 use crate::core::memory_boundary::FactPrivacy;
40 use crate::core::memory_policy::MemoryPolicy;
41 use chrono::Utc;
42
43 fn default_policy() -> MemoryPolicy {
44 MemoryPolicy::default()
45 }
46
47 fn current_count(k: &ProjectKnowledge) -> usize {
48 k.facts.iter().filter(|f| f.is_current()).count()
49 }
50
51 #[test]
54 fn admission_merges_same_category_near_duplicate_without_growing() {
55 let policy = default_policy(); let k_root = tempfile::tempdir().unwrap();
57 let mut k = ProjectKnowledge::new(k_root.path().to_str().unwrap());
58
59 let r1 = k.remember_admitted(
60 "architecture",
61 "db-primary",
62 "use postgres for the primary datastore",
63 "s1",
64 0.8,
65 &policy,
66 );
67 assert!(matches!(r1, AdmissionResult::Stored(_)));
68 assert_eq!(current_count(&k), 1);
69
70 let r2 = k.remember_admitted(
72 "architecture",
73 "db-store",
74 "use postgres for the primary datastore",
75 "s2",
76 0.9,
77 &policy,
78 );
79 match r2 {
80 AdmissionResult::Merged {
81 category,
82 key,
83 confirmations,
84 ..
85 } => {
86 assert_eq!(category, "architecture");
87 assert_eq!(key, "db-primary", "merges into the earliest match");
88 assert_eq!(confirmations, 2);
89 }
90 other => panic!("expected Merged, got {other:?}"),
91 }
92 assert_eq!(current_count(&k), 1, "merge must not grow the store");
93 let f = k
94 .facts
95 .iter()
96 .find(|f| f.key == "db-primary" && f.is_current())
97 .unwrap();
98 assert_eq!(f.confirmation_count, 2);
99 }
100
101 #[test]
102 fn admission_rejects_below_salience_floor() {
103 let mut policy = default_policy();
104 policy.admission.min_salience = 100; let k_root = tempfile::tempdir().unwrap();
107
108 let mut k = ProjectKnowledge::new(k_root.path().to_str().unwrap());
109 let r = k.remember_admitted("misc", "note", "a quiet plain note", "s1", 0.8, &policy);
110 assert!(matches!(r, AdmissionResult::RejectedLowSalience { .. }));
111 assert_eq!(current_count(&k), 0, "rejected fact is never stored");
112
113 let r2 = k.remember_admitted(
115 "misc",
116 "bug",
117 "security error: auth failed with timeout",
118 "s1",
119 0.8,
120 &policy,
121 );
122 assert!(matches!(r2, AdmissionResult::Stored(_)));
123 assert_eq!(current_count(&k), 1);
124 }
125
126 #[test]
127 fn admission_preserves_contradiction_supersede_on_same_key() {
128 let policy = default_policy();
129 let k_root = tempfile::tempdir().unwrap();
130 let mut k = ProjectKnowledge::new(k_root.path().to_str().unwrap());
131
132 let r1 = k.remember_admitted("arch", "db", "PostgreSQL", "s1", 0.95, &policy);
133 assert!(matches!(r1, AdmissionResult::Stored(None)));
134
135 let r2 = k.remember_admitted("arch", "db", "MySQL", "s2", 0.9, &policy);
137 assert!(
138 matches!(r2, AdmissionResult::Stored(Some(_))),
139 "same-key conflict must surface a contradiction, got {r2:?}"
140 );
141 let cur = k
142 .facts
143 .iter()
144 .find(|f| f.category == "arch" && f.key == "db" && f.is_current())
145 .unwrap();
146 assert_eq!(cur.value, "MySQL");
147 }
148
149 #[test]
150 fn admission_disabled_inserts_like_legacy() {
151 let mut policy = default_policy();
152 policy.admission.enabled = false;
153
154 let k_root = tempfile::tempdir().unwrap();
155
156 let mut k = ProjectKnowledge::new(k_root.path().to_str().unwrap());
157 k.remember_admitted(
158 "architecture",
159 "a",
160 "use postgres for the primary datastore",
161 "s1",
162 0.8,
163 &policy,
164 );
165 let r = k.remember_admitted(
166 "architecture",
167 "b",
168 "use postgres for the primary datastore",
169 "s2",
170 0.9,
171 &policy,
172 );
173 assert!(
174 matches!(r, AdmissionResult::Stored(_)),
175 "disabled admission never merges"
176 );
177 assert_eq!(current_count(&k), 2, "both distinct keys stored");
178 }
179
180 #[test]
181 fn admission_does_not_merge_across_categories() {
182 let policy = default_policy();
183 let k_root = tempfile::tempdir().unwrap();
184 let mut k = ProjectKnowledge::new(k_root.path().to_str().unwrap());
185
186 k.remember_admitted(
187 "architecture",
188 "db",
189 "use postgres for the primary datastore",
190 "s1",
191 0.8,
192 &policy,
193 );
194 let r = k.remember_admitted(
196 "decision",
197 "db",
198 "use postgres for the primary datastore",
199 "s2",
200 0.9,
201 &policy,
202 );
203 assert!(matches!(r, AdmissionResult::Stored(_)));
204 assert_eq!(current_count(&k), 2);
205 }
206
207 #[test]
208 fn remember_and_recall() {
209 let policy = default_policy();
210 let k_root = tempfile::tempdir().unwrap();
211 let mut k = ProjectKnowledge::new(k_root.path().to_str().unwrap());
212 k.remember(
213 "architecture",
214 "auth",
215 "JWT RS256",
216 "session-1",
217 0.9,
218 &policy,
219 );
220 k.remember("api", "rate-limit", "100/min", "session-1", 0.8, &policy);
221
222 let results = k.recall("auth");
223 assert_eq!(results.len(), 1);
224 assert_eq!(results[0].value, "JWT RS256");
225
226 let results = k.recall("api rate");
227 assert_eq!(results.len(), 1);
228 assert_eq!(results[0].key, "rate-limit");
229 }
230
231 #[test]
234 fn recall_splits_hyphenated_upper_case_queries_like_the_index() {
235 let policy = default_policy();
236 let k_root = tempfile::tempdir().unwrap();
237 let mut k = ProjectKnowledge::new(k_root.path().to_str().unwrap());
238 k.remember(
239 "decision",
240 "local-read-docs",
241 "Local-Read docs answer unavailable on a broken vault map",
242 "s1",
243 0.8,
244 &policy,
245 );
246 for query in ["Local-Read", "LOCAL-READ unavailable", "local_read/Docs"] {
247 assert_eq!(k.recall(query).len(), 1, "{query}");
248 assert_eq!(k.recall_at_time(query, Utc::now()).len(), 1, "{query}");
249 assert_eq!(k.recall_for_output(query, 5).0.len(), 1, "{query}");
250 }
251 assert!(k.recall("-- / .").is_empty());
252 }
253
254 #[test]
255 fn recall_for_task_matches_content_words_only() {
256 let policy = default_policy();
257 let k_root = tempfile::tempdir().unwrap();
258 let mut k = ProjectKnowledge::new(k_root.path().to_str().unwrap());
259 k.remember(
260 "finding",
261 "deploy",
262 "Review the deploy order",
263 "s1",
264 0.8,
265 &policy,
266 );
267 k.remember("api", "rate-limit", "100/min", "s1", 0.8, &policy);
268
269 assert!(
271 k.recall_for_task("Review the rate limit.")
272 .iter()
273 .all(|f| f.key != "deploy")
274 );
275 assert_eq!(k.recall("review").len(), 1);
276
277 let hits = k.recall_for_task("Check rate_limit handling.");
279 assert_eq!(hits.len(), 1);
280 assert_eq!(hits[0].key, "rate-limit");
281
282 assert!(k.recall_for_task("Please review and fix this.").is_empty());
283 }
284
285 #[test]
286 fn remember_infers_archetype_from_category() {
287 let policy = default_policy();
288 let k_root = tempfile::tempdir().unwrap();
289 let mut k = ProjectKnowledge::new(k_root.path().to_str().unwrap());
290 k.remember("architecture", "db", "PostgreSQL", "s1", 0.9, &policy);
291 k.remember("decision", "lang", "Rust", "s1", 0.9, &policy);
292 k.remember("known_bugs", "src/a.rs:1", "panics", "s1", 0.9, &policy);
293 k.remember("data_model", "user", "id,email", "s1", 0.9, &policy);
294 k.remember("misc", "note", "whatever", "s1", 0.9, &policy);
295
296 let arch = |cat: &str| {
297 k.facts
298 .iter()
299 .find(|f| f.category == cat && f.is_current())
300 .map(|f| f.archetype.clone())
301 .expect("fact present")
302 };
303 assert_eq!(arch("architecture"), KnowledgeArchetype::Architecture);
304 assert_eq!(arch("decision"), KnowledgeArchetype::Decision);
305 assert_eq!(arch("known_bugs"), KnowledgeArchetype::Gotcha);
306 assert_eq!(arch("data_model"), KnowledgeArchetype::Architecture);
307 assert_eq!(arch("misc"), KnowledgeArchetype::Fact);
308 }
309
310 #[test]
311 fn recall_surfaces_synthesized_observation_first() {
312 let policy = default_policy();
313 let k_root = tempfile::tempdir().unwrap();
314 let mut k = ProjectKnowledge::new(k_root.path().to_str().unwrap());
315 k.remember(
317 "decision",
318 "auth-choice",
319 "auth module uses jwt tokens",
320 "s1",
321 0.6,
322 &policy,
323 );
324 k.remember(
325 "observation",
326 "src/auth.rs",
327 "auth module summary jwt tokens",
328 crate::core::knowledge::COGNITION_SYNTHESIS_SOURCE,
329 0.6,
330 &policy,
331 );
332
333 let (out, total) = k.recall_for_output("jwt", 10);
334 assert!(total >= 2, "both facts match the query");
335 assert!(
336 out[0].is_synthesized_observation(),
337 "synthesized observation ranks above an equally relevant raw fact"
338 );
339 assert_eq!(out[0].key, "src/auth.rs");
340 }
341
342 #[test]
343 fn recall_exact_key_hit_outranks_observation() {
344 let _iso = crate::core::data_dir::isolated_data_dir();
351 let policy = default_policy();
352 let k_root = tempfile::tempdir().unwrap();
353 let mut k = ProjectKnowledge::new(k_root.path().to_str().unwrap());
354 k.remember(
355 "decision",
356 "jwt",
357 "auth module uses jwt tokens",
358 "s1",
359 0.6,
360 &policy,
361 );
362 k.remember(
363 "observation",
364 "src/auth.rs",
365 "auth module summary jwt tokens",
366 crate::core::knowledge::COGNITION_SYNTHESIS_SOURCE,
367 0.6,
368 &policy,
369 );
370
371 let (out, _total) = k.recall_for_output("jwt", 10);
372 assert_eq!(out[0].key, "jwt", "exact key hit leads the ranking");
373 assert_eq!(out[0].category, "decision");
374 assert!(
375 out.iter()
376 .any(crate::core::knowledge::KnowledgeFact::is_synthesized_observation),
377 "the observation is still recalled, just below the exact hit"
378 );
379 }
380
381 #[test]
382 fn writes_never_evict_facts_by_count() {
383 let _iso = crate::core::data_dir::isolated_data_dir();
384 let root = tempfile::tempdir().unwrap();
385 let mut policy = default_policy();
389 policy.knowledge.max_facts = Some(5);
390 let mut k = ProjectKnowledge::new(root.path().to_str().unwrap());
391 for i in 0..40 {
392 k.remember(
393 "finding",
394 &format!("key-{i}"),
395 &format!("value number {i}"),
396 "s1",
397 0.7,
398 &policy,
399 );
400 }
401 assert_eq!(k.facts.len(), 40);
402
403 let other = tempfile::tempdir().unwrap();
404 let mut imported = ProjectKnowledge::new(other.path().to_str().unwrap());
405 imported.import_facts(k.facts.clone(), ImportMerge::Append, "s2");
406 assert_eq!(imported.facts.len(), 40);
407
408 let report = k.run_memory_lifecycle(&policy).expect("lifecycle");
409 assert_eq!(report.archived_count, 0);
410 assert_eq!(k.facts.len(), 40, "healthy facts stay over max_facts");
411 }
412
413 #[test]
417 fn a_cached_store_follows_every_write() {
418 let _iso = crate::core::data_dir::isolated_data_dir();
419 let root = tempfile::tempdir().unwrap();
420 let root = root.path().to_str().unwrap();
421 let policy = default_policy();
422 ProjectKnowledge::mutate_locked(root, |k| {
423 k.remember("decision", "first", "first value", "s1", 0.9, &policy);
424 })
425 .unwrap();
426 let keys = |k: &ProjectKnowledge| {
427 let mut keys: Vec<String> = k.facts.iter().map(|f| f.key.clone()).collect();
428 keys.sort();
429 keys
430 };
431 assert_eq!(keys(&ProjectKnowledge::load(root).unwrap()), ["first"]);
432 assert_eq!(keys(&ProjectKnowledge::load(root).unwrap()), ["first"]);
433
434 ProjectKnowledge::mutate_locked(root, |k| {
435 k.remember("decision", "second", "second value", "s1", 0.9, &policy);
436 })
437 .unwrap();
438 assert_eq!(
439 keys(&ProjectKnowledge::load(root).unwrap()),
440 ["first", "second"]
441 );
442
443 let path = crate::core::data_dir::lean_ctx_data_dir()
444 .unwrap()
445 .join("knowledge")
446 .join(super::ranking::hash_project_root(root))
447 .join("knowledge.json");
448 let text = std::fs::read_to_string(&path).unwrap();
449 std::fs::write(
450 &path,
451 text.replace("second value", "edited in place, longer"),
452 )
453 .unwrap();
454 let loaded = ProjectKnowledge::load(root).unwrap();
455 assert!(
456 loaded
457 .facts
458 .iter()
459 .any(|f| f.value == "edited in place, longer"),
460 "an in-place edit is seen"
461 );
462 }
463
464 #[test]
469 fn a_store_over_eight_mib_stays_writable_and_recallable() {
470 let _iso = crate::core::data_dir::isolated_data_dir();
471 let root = tempfile::tempdir().unwrap();
472 let root = root.path().to_str().unwrap();
473 let policy = default_policy();
474 let mut k = ProjectKnowledge::new(root);
475 k.remember("finding", "seed", "seed value", "s1", 0.7, &policy);
476 let template = k.facts[0].clone();
477 let filler = "x".repeat(800);
478 for i in 0..12_000 {
479 let mut fact = template.clone();
480 fact.key = format!("bulk-{i}");
481 fact.value = format!("bulk fact {i} {filler}");
482 k.facts.push(fact);
483 }
484 k.rebuild_index();
485 let size = serde_json::to_string_pretty(&k).unwrap().len();
486 assert!(
487 size > crate::core::policy::content::MAX_PROTECTED_CONTENT_BYTES,
488 "{size}"
489 );
490 k.save().expect("a large store is saved");
491
492 let started = std::time::Instant::now();
493 let (stored, ()) = ProjectKnowledge::mutate_locked(root, |k| {
494 k.remember("decision", "needle", "the needle fact", "s2", 0.9, &policy);
495 })
496 .expect("remember into a store over the cap");
497 assert_eq!(stored.facts.len(), 12_002);
498 let loaded = ProjectKnowledge::load(root).expect("load");
499 assert!(loaded.recall("needle").iter().any(|f| f.key == "needle"));
500 let elapsed = started.elapsed();
501 assert!(
506 std::env::var("LEAN_CTX_PERF_GATE").as_deref() != Ok("1")
507 || elapsed < std::time::Duration::from_secs(30),
508 "remember + recall took {elapsed:?}"
509 );
510 }
511
512 #[test]
513 fn upsert_existing_fact() {
514 let policy = default_policy();
515 let k_root = tempfile::tempdir().unwrap();
516 let mut k = ProjectKnowledge::new(k_root.path().to_str().unwrap());
517 k.remember("arch", "db", "PostgreSQL", "s1", 0.7, &policy);
518 k.remember(
519 "arch",
520 "db",
521 "PostgreSQL 16 with pgvector",
522 "s2",
523 0.95,
524 &policy,
525 );
526
527 let current: Vec<_> = k.facts.iter().filter(|f| f.is_current()).collect();
528 assert_eq!(current.len(), 1);
529 assert_eq!(current[0].value, "PostgreSQL 16 with pgvector");
530 }
531
532 #[test]
533 fn contradiction_detection() {
534 let policy = default_policy();
535 let k_root = tempfile::tempdir().unwrap();
536 let mut k = ProjectKnowledge::new(k_root.path().to_str().unwrap());
537 k.remember("arch", "db", "PostgreSQL", "s1", 0.95, &policy);
538 k.facts[0].confirmation_count = 3;
539
540 let contradiction = k.check_contradiction("arch", "db", "MySQL", &policy);
541 assert!(contradiction.is_some());
542 let c = contradiction.unwrap();
543 assert_eq!(c.severity, ContradictionSeverity::High);
544 }
545
546 #[test]
547 fn temporal_validity() {
548 let policy = default_policy();
549 let k_root = tempfile::tempdir().unwrap();
550 let mut k = ProjectKnowledge::new(k_root.path().to_str().unwrap());
551 k.remember("arch", "db", "PostgreSQL", "s1", 0.95, &policy);
552 k.facts[0].confirmation_count = 3;
553
554 k.remember("arch", "db", "MySQL", "s2", 0.9, &policy);
555
556 let current: Vec<_> = k.facts.iter().filter(|f| f.is_current()).collect();
557 assert_eq!(current.len(), 1);
558 assert_eq!(current[0].value, "MySQL");
559
560 let all_db: Vec<_> = k.facts.iter().filter(|f| f.key == "db").collect();
561 assert_eq!(all_db.len(), 2);
562 }
563
564 #[test]
565 fn confirmation_count() {
566 let policy = default_policy();
567 let k_root = tempfile::tempdir().unwrap();
568 let mut k = ProjectKnowledge::new(k_root.path().to_str().unwrap());
569 k.remember("arch", "db", "PostgreSQL", "s1", 0.9, &policy);
570 assert_eq!(k.facts[0].confirmation_count, 1);
571
572 k.remember("arch", "db", "PostgreSQL", "s2", 0.9, &policy);
573 assert_eq!(k.facts[0].confirmation_count, 2);
574 }
575
576 #[test]
577 fn remove_fact() {
578 let policy = default_policy();
579 let k_root = tempfile::tempdir().unwrap();
580 let mut k = ProjectKnowledge::new(k_root.path().to_str().unwrap());
581 k.remember("arch", "db", "PostgreSQL", "s1", 0.9, &policy);
582 assert!(k.remove_fact("arch", "db"));
583 assert!(k.facts.is_empty());
584 assert!(!k.remove_fact("arch", "db"));
585 }
586
587 #[test]
588 fn list_rooms() {
589 let policy = default_policy();
590 let k_root = tempfile::tempdir().unwrap();
591 let mut k = ProjectKnowledge::new(k_root.path().to_str().unwrap());
592 k.remember("architecture", "auth", "JWT", "s1", 0.9, &policy);
593 k.remember("architecture", "db", "PG", "s1", 0.9, &policy);
594 k.remember("deploy", "host", "AWS", "s1", 0.8, &policy);
595
596 let rooms = k.list_rooms();
597 assert_eq!(rooms.len(), 2);
598 }
599
600 #[test]
601 fn aaak_format() {
602 let policy = default_policy();
603 let k_root = tempfile::tempdir().unwrap();
604 let mut k = ProjectKnowledge::new(k_root.path().to_str().unwrap());
605 k.remember("architecture", "auth", "JWT RS256", "s1", 0.95, &policy);
606 k.remember("architecture", "db", "PostgreSQL", "s1", 0.7, &policy);
607
608 let aaak = k.format_aaak();
609 assert!(aaak.contains("ARCHITECTURE:"));
610 assert!(aaak.contains("auth=JWT RS256"));
611 }
612
613 #[test]
614 fn consolidate_history() {
615 let policy = default_policy();
616 let k_root = tempfile::tempdir().unwrap();
617 let mut k = ProjectKnowledge::new(k_root.path().to_str().unwrap());
618 k.consolidate(
619 "Migrated from REST to GraphQL",
620 vec!["s1".into(), "s2".into()],
621 &policy,
622 )
623 .expect("consolidation succeeds");
624 assert_eq!(k.history.len(), 1);
625 assert_eq!(k.history[0].from_sessions.len(), 2);
626 }
627
628 #[test]
629 fn format_summary_output() {
630 let policy = default_policy();
631 let k_root = tempfile::tempdir().unwrap();
632 let mut k = ProjectKnowledge::new(k_root.path().to_str().unwrap());
633 k.remember("architecture", "auth", "JWT RS256", "s1", 0.9, &policy);
634 k.add_pattern(
635 "naming",
636 "snake_case for functions",
637 vec!["get_user()".into()],
638 "s1",
639 &policy,
640 );
641 let summary = k.format_summary();
642 assert!(summary.contains("PROJECT KNOWLEDGE:"));
643 assert!(summary.contains("auth: JWT RS256"));
644 assert!(summary.contains("PROJECT PATTERNS:"));
645 }
646
647 #[test]
648 fn temporal_recall_at_time() {
649 let policy = default_policy();
650 let k_root = tempfile::tempdir().unwrap();
651 let mut k = ProjectKnowledge::new(k_root.path().to_str().unwrap());
652 k.remember("arch", "db", "PostgreSQL", "s1", 0.95, &policy);
653 k.facts[0].confirmation_count = 3;
654
655 let before_change = Utc::now();
656 std::thread::sleep(std::time::Duration::from_millis(10));
657
658 k.remember("arch", "db", "MySQL", "s2", 0.9, &policy);
659
660 let results = k.recall_at_time("db", before_change);
661 assert_eq!(results.len(), 1);
662 assert_eq!(results[0].value, "PostgreSQL");
663
664 let results_now = k.recall_at_time("db", Utc::now());
665 assert_eq!(results_now.len(), 1);
666 assert_eq!(results_now[0].value, "MySQL");
667 }
668
669 #[test]
670 fn timeline_shows_history() {
671 let policy = default_policy();
672 let k_root = tempfile::tempdir().unwrap();
673 let mut k = ProjectKnowledge::new(k_root.path().to_str().unwrap());
674 k.remember("arch", "db", "PostgreSQL", "s1", 0.95, &policy);
675 k.facts[0].confirmation_count = 3;
676 k.remember("arch", "db", "MySQL", "s2", 0.9, &policy);
677
678 let timeline = k.timeline("arch");
679 assert_eq!(timeline.len(), 2);
680 assert!(!timeline[0].is_current());
681 assert!(timeline[1].is_current());
682 }
683
684 #[test]
685 fn wakeup_format() {
686 let policy = default_policy();
687 let k_root = tempfile::tempdir().unwrap();
688 let mut k = ProjectKnowledge::new(k_root.path().to_str().unwrap());
689 k.remember("arch", "auth", "JWT", "s1", 0.95, &policy);
690 k.remember("arch", "db", "PG", "s1", 0.8, &policy);
691
692 let wakeup = k.format_wakeup();
695 assert!(wakeup.contains("FACTS:"));
696 assert!(wakeup.contains("[arch]"));
697 assert!(wakeup.contains("auth=JWT"));
698 assert!(wakeup.contains("db=PG"));
699 }
700
701 #[test]
702 fn salience_prioritizes_decisions_over_findings_at_similar_confidence() {
703 let policy = default_policy();
704 let k_root = tempfile::tempdir().unwrap();
705 let mut k = ProjectKnowledge::new(k_root.path().to_str().unwrap());
706 k.remember("finding", "f1", "some thing", "s1", 0.9, &policy);
707 k.remember("decision", "d1", "important", "s1", 0.85, &policy);
708
709 let wakeup = k.format_wakeup();
712 let d = wakeup.find("d1=important").expect("decision in wakeup");
713 let f = wakeup.find("f1=some thing").expect("finding in wakeup");
714 assert!(
715 d < f,
716 "expected decision before finding in wakeup: {wakeup}"
717 );
718 }
719
720 #[test]
721 fn low_confidence_contradiction() {
722 let policy = default_policy();
723 let k_root = tempfile::tempdir().unwrap();
724 let mut k = ProjectKnowledge::new(k_root.path().to_str().unwrap());
725 k.remember("arch", "db", "PostgreSQL", "s1", 0.4, &policy);
726
727 let c = k.check_contradiction("arch", "db", "MySQL", &policy);
728 assert!(c.is_some());
729 assert_eq!(c.unwrap().severity, ContradictionSeverity::Low);
730 }
731
732 #[test]
733 fn no_contradiction_for_same_value() {
734 let policy = default_policy();
735 let k_root = tempfile::tempdir().unwrap();
736 let mut k = ProjectKnowledge::new(k_root.path().to_str().unwrap());
737 k.remember("arch", "db", "PostgreSQL", "s1", 0.95, &policy);
738
739 let c = k.check_contradiction("arch", "db", "PostgreSQL", &policy);
740 assert!(c.is_none());
741 }
742
743 #[test]
744 fn no_contradiction_for_similar_values() {
745 let policy = default_policy();
746 let k_root = tempfile::tempdir().unwrap();
747 let mut k = ProjectKnowledge::new(k_root.path().to_str().unwrap());
748 k.remember(
749 "arch",
750 "db",
751 "PostgreSQL 16 production database server",
752 "s1",
753 0.95,
754 &policy,
755 );
756
757 let c = k.check_contradiction(
758 "arch",
759 "db",
760 "PostgreSQL 16 production database server config",
761 &policy,
762 );
763 assert!(c.is_none());
764 }
765
766 #[test]
767 fn import_skip_existing() {
768 let policy = default_policy();
769 let k_root = tempfile::tempdir().unwrap();
770 let mut k = ProjectKnowledge::new(k_root.path().to_str().unwrap());
771 k.remember("arch", "db", "PostgreSQL", "s1", 0.95, &policy);
772
773 let incoming = vec![KnowledgeFact {
774 anchors: Vec::new(),
775 review: None,
776 capture: Default::default(),
777 origin: crate::core::knowledge::FactOrigin::Local,
778 category: "arch".into(),
779 key: "db".into(),
780 value: "MySQL".into(),
781 source_session: "import".into(),
782 confidence: 0.8,
783 created_at: Utc::now(),
784 last_confirmed: Utc::now(),
785 retrieval_count: 0,
786 last_retrieved: None,
787 valid_from: Some(Utc::now()),
788 valid_until: None,
789 supersedes: None,
790 confirmation_count: 1,
791 feedback_up: 0,
792 feedback_down: 0,
793 last_feedback: None,
794 privacy: FactPrivacy::default(),
795 sensitivity: crate::core::sensitivity::SensitivityLevel::default(),
796 imported_from: None,
797 archetype: KnowledgeArchetype::default(),
798 fidelity: None,
799 revision_count: 0,
800 status: None,
801 validity: None,
802 }];
803
804 let result = k.import_facts(incoming, ImportMerge::SkipExisting, "imp-1");
805 assert_eq!(result.skipped, 1);
806 assert_eq!(result.added, 0);
807 assert_eq!(k.facts.iter().filter(|f| f.is_current()).count(), 1);
808 }
809
810 #[test]
811 fn import_replace_existing() {
812 let policy = default_policy();
813 let k_root = tempfile::tempdir().unwrap();
814 let mut k = ProjectKnowledge::new(k_root.path().to_str().unwrap());
815 k.remember("arch", "db", "PostgreSQL", "s1", 0.95, &policy);
816
817 let incoming = vec![KnowledgeFact {
818 anchors: Vec::new(),
819 review: None,
820 capture: Default::default(),
821 origin: crate::core::knowledge::FactOrigin::Local,
822 category: "arch".into(),
823 key: "db".into(),
824 value: "MySQL".into(),
825 source_session: "import".into(),
826 confidence: 0.8,
827 created_at: Utc::now(),
828 last_confirmed: Utc::now(),
829 retrieval_count: 0,
830 last_retrieved: None,
831 valid_from: Some(Utc::now()),
832 valid_until: None,
833 supersedes: None,
834 confirmation_count: 1,
835 feedback_up: 0,
836 feedback_down: 0,
837 last_feedback: None,
838 privacy: FactPrivacy::default(),
839 sensitivity: crate::core::sensitivity::SensitivityLevel::default(),
840 imported_from: None,
841 archetype: KnowledgeArchetype::default(),
842 fidelity: None,
843 revision_count: 0,
844 status: None,
845 validity: None,
846 }];
847
848 let result = k.import_facts(incoming, ImportMerge::Replace, "imp-1");
849 assert_eq!(result.replaced, 1);
850 let current: Vec<_> = k.facts.iter().filter(|f| f.is_current()).collect();
851 assert_eq!(current.len(), 1);
852 assert_eq!(current[0].value, "MySQL");
853 }
854
855 #[test]
856 fn import_adds_new_facts() {
857 let policy = default_policy();
858 let k_root = tempfile::tempdir().unwrap();
859 let mut k = ProjectKnowledge::new(k_root.path().to_str().unwrap());
860 k.remember("arch", "db", "PostgreSQL", "s1", 0.95, &policy);
861
862 let incoming = vec![KnowledgeFact {
863 anchors: Vec::new(),
864 review: None,
865 capture: Default::default(),
866 origin: crate::core::knowledge::FactOrigin::Local,
867 category: "security".into(),
868 key: "auth".into(),
869 value: "JWT".into(),
870 source_session: "import".into(),
871 confidence: 0.9,
872 created_at: Utc::now(),
873 last_confirmed: Utc::now(),
874 retrieval_count: 0,
875 last_retrieved: None,
876 valid_from: Some(Utc::now()),
877 valid_until: None,
878 supersedes: None,
879 confirmation_count: 1,
880 feedback_up: 0,
881 feedback_down: 0,
882 last_feedback: None,
883 privacy: FactPrivacy::default(),
884 sensitivity: crate::core::sensitivity::SensitivityLevel::default(),
885 imported_from: None,
886 archetype: KnowledgeArchetype::default(),
887 fidelity: None,
888 revision_count: 0,
889 status: None,
890 validity: None,
891 }];
892
893 let result = k.import_facts(incoming, ImportMerge::SkipExisting, "imp-1");
894 assert_eq!(result.added, 1);
895 assert_eq!(k.facts.iter().filter(|f| f.is_current()).count(), 2);
896 }
897
898 #[test]
899 fn parse_simple_json_array() {
900 let data = r#"[
901 {"category": "arch", "key": "db", "value": "PostgreSQL"},
902 {"category": "security", "key": "auth", "value": "JWT", "confidence": 0.9}
903 ]"#;
904 let facts = parse_import_data(data).unwrap();
905 assert_eq!(facts.len(), 2);
906 assert_eq!(facts[0].category, "arch");
907 assert_eq!(facts[1].confidence, 0.9);
908 }
909
910 #[test]
911 fn parse_jsonl_format() {
912 let data = "{\"category\":\"arch\",\"key\":\"db\",\"value\":\"PG\"}\n\
913 {\"category\":\"security\",\"key\":\"auth\",\"value\":\"JWT\"}";
914 let facts = parse_import_data(data).unwrap();
915 assert_eq!(facts.len(), 2);
916 }
917
918 #[test]
919 fn export_simple_only_current() {
920 let policy = default_policy();
921 let k_root = tempfile::tempdir().unwrap();
922 let mut k = ProjectKnowledge::new(k_root.path().to_str().unwrap());
923 k.remember("arch", "db", "PostgreSQL", "s1", 0.95, &policy);
924 k.remember("arch", "db", "MySQL", "s2", 0.9, &policy);
925
926 let exported = k.export_simple();
927 assert_eq!(exported.len(), 1);
928 assert_eq!(exported[0].value, "MySQL");
929 }
930
931 #[test]
932 fn import_merge_parse() {
933 assert_eq!(ImportMerge::parse("replace"), Some(ImportMerge::Replace));
934 assert_eq!(ImportMerge::parse("append"), Some(ImportMerge::Append));
935 assert_eq!(
936 ImportMerge::parse("skip-existing"),
937 Some(ImportMerge::SkipExisting)
938 );
939 assert_eq!(
940 ImportMerge::parse("skip_existing"),
941 Some(ImportMerge::SkipExisting)
942 );
943 assert_eq!(ImportMerge::parse("skip"), Some(ImportMerge::SkipExisting));
944 assert!(ImportMerge::parse("invalid").is_none());
945 }
946
947 #[test]
948 fn revision_count_on_new_fact() {
949 let policy = default_policy();
950 let k_root = tempfile::tempdir().unwrap();
951 let mut k = ProjectKnowledge::new(k_root.path().to_str().unwrap());
952 k.remember("arch", "db", "PostgreSQL", "s1", 0.9, &policy);
953 let cur = k.facts.iter().find(|f| f.is_current()).unwrap();
954 assert_eq!(cur.revision_count, 1);
955 }
956
957 #[test]
958 fn revision_count_increments_on_confirm() {
959 let policy = default_policy();
960 let k_root = tempfile::tempdir().unwrap();
961 let mut k = ProjectKnowledge::new(k_root.path().to_str().unwrap());
962 k.remember("arch", "db", "PostgreSQL", "s1", 0.9, &policy);
963 k.remember("arch", "db", "PostgreSQL", "s2", 0.9, &policy);
964 k.remember("arch", "db", "PostgreSQL", "s3", 0.9, &policy);
965 let cur = k.facts.iter().find(|f| f.is_current()).unwrap();
966 assert_eq!(cur.revision_count, 3);
967 assert_eq!(cur.confirmation_count, 3);
968 }
969
970 #[test]
971 fn revision_count_carries_over_on_supersede() {
972 let policy = default_policy();
973 let k_root = tempfile::tempdir().unwrap();
974 let mut k = ProjectKnowledge::new(k_root.path().to_str().unwrap());
975 k.remember("arch", "db", "PostgreSQL", "s1", 0.95, &policy);
976 k.remember("arch", "db", "PostgreSQL", "s2", 0.9, &policy);
977 assert_eq!(
978 k.facts
979 .iter()
980 .find(|f| f.is_current())
981 .unwrap()
982 .revision_count,
983 2
984 );
985 k.facts[0].confirmation_count = 3;
986 k.remember("arch", "db", "MySQL", "s3", 0.9, &policy);
987 let cur: Vec<_> = k.facts.iter().filter(|f| f.is_current()).collect();
988 assert_eq!(cur.len(), 1);
989 assert_eq!(cur[0].value, "MySQL");
990 assert_eq!(cur[0].revision_count, 3);
991 assert!(cur[0].supersedes.is_some());
992 }
993
994 #[test]
995 fn revision_count_default_zero_for_legacy() {
996 let json = r#"{
997 "category": "test", "key": "k", "value": "v",
998 "source_session": "s", "confidence": 0.8,
999 "created_at": "2024-01-01T00:00:00Z",
1000 "last_confirmed": "2024-01-01T00:00:00Z"
1001 }"#;
1002 let fact: KnowledgeFact = serde_json::from_str(json).unwrap();
1003 assert_eq!(fact.revision_count, 0);
1004 }
1005
1006 #[test]
1007 fn judged_pairs_default_empty_for_legacy() {
1008 let json = r#"{
1009 "project_root": "/test", "project_hash": "abc",
1010 "facts": [], "patterns": [], "history": [],
1011 "updated_at": "2024-01-01T00:00:00Z"
1012 }"#;
1013 let pk: ProjectKnowledge = serde_json::from_str(json).unwrap();
1014 assert!(pk.judged_pairs.is_empty());
1015 }
1016
1017 #[test]
1018 fn cross_key_similar_finds_related_facts() {
1019 let policy = default_policy();
1020 let k_root = tempfile::tempdir().unwrap();
1021 let mut k = ProjectKnowledge::new(k_root.path().to_str().unwrap());
1022 k.remember(
1023 "architecture",
1024 "auth",
1025 "JWT RS256 token based authentication with Redis session store",
1026 "s1",
1027 0.9,
1028 &policy,
1029 );
1030 k.remember(
1031 "decision",
1032 "session-model",
1033 "JWT token authentication stored in Redis for session management",
1034 "s1",
1035 0.85,
1036 &policy,
1037 );
1038 k.remember("deploy", "host", "AWS us-east-1", "s1", 0.8, &policy);
1039
1040 let similar = find_cross_key_similar(
1041 "architecture",
1042 "auth",
1043 "JWT RS256 token based authentication with Redis session store",
1044 &k.facts,
1045 &k.judged_pairs,
1046 3,
1047 );
1048 assert!(!similar.is_empty(), "should find session-model as similar");
1049 assert_eq!(similar[0].category, "decision");
1050 assert_eq!(similar[0].key, "session-model");
1051 assert!(similar[0].similarity > 0.35);
1052 }
1053
1054 #[test]
1055 fn cross_key_similar_excludes_same_key() {
1056 let policy = default_policy();
1057 let k_root = tempfile::tempdir().unwrap();
1058 let mut k = ProjectKnowledge::new(k_root.path().to_str().unwrap());
1059 k.remember("arch", "db", "PostgreSQL 16", "s1", 0.9, &policy);
1060
1061 let similar =
1062 find_cross_key_similar("arch", "db", "PostgreSQL 16", &k.facts, &k.judged_pairs, 3);
1063 assert!(similar.is_empty());
1064 }
1065
1066 #[test]
1067 fn cross_key_similar_excludes_judged_pairs() {
1068 let policy = default_policy();
1069 let k_root = tempfile::tempdir().unwrap();
1070 let mut k = ProjectKnowledge::new(k_root.path().to_str().unwrap());
1071 k.remember(
1072 "architecture",
1073 "auth",
1074 "JWT RS256 token based authentication with Redis",
1075 "s1",
1076 0.9,
1077 &policy,
1078 );
1079 k.remember(
1080 "decision",
1081 "session-model",
1082 "JWT token authentication stored in Redis",
1083 "s1",
1084 0.85,
1085 &policy,
1086 );
1087
1088 k.judged_pairs.push(JudgedPair {
1089 key_a: "architecture/auth".into(),
1090 key_b: "decision/session-model".into(),
1091 verdict: "compatible".into(),
1092 judged_at: Utc::now(),
1093 });
1094
1095 let similar = find_cross_key_similar(
1096 "architecture",
1097 "auth",
1098 "JWT RS256 token based authentication with Redis",
1099 &k.facts,
1100 &k.judged_pairs,
1101 3,
1102 );
1103 assert!(similar.is_empty(), "judged pairs should be excluded");
1104 }
1105
1106 #[test]
1107 fn cross_key_similar_ignores_unrelated_facts() {
1108 let policy = default_policy();
1109 let k_root = tempfile::tempdir().unwrap();
1110 let mut k = ProjectKnowledge::new(k_root.path().to_str().unwrap());
1111 k.remember(
1112 "arch",
1113 "db",
1114 "PostgreSQL 16 with pgvector",
1115 "s1",
1116 0.9,
1117 &policy,
1118 );
1119 k.remember("deploy", "host", "AWS us-east-1 region", "s1", 0.8, &policy);
1120
1121 let similar = find_cross_key_similar(
1122 "arch",
1123 "db",
1124 "PostgreSQL 16 with pgvector",
1125 &k.facts,
1126 &k.judged_pairs,
1127 3,
1128 );
1129 assert!(similar.is_empty(), "unrelated facts should not match");
1130 }
1131
1132 #[test]
1133 fn judge_supersedes_archives_target() {
1134 let policy = default_policy();
1135 let k_root = tempfile::tempdir().unwrap();
1136 let mut k = ProjectKnowledge::new(k_root.path().to_str().unwrap());
1137 k.remember("architecture", "auth", "JWT RS256", "s1", 0.9, &policy);
1138 k.remember("decision", "session", "JWT tokens", "s1", 0.85, &policy);
1139
1140 assert!(k.facts.iter().all(KnowledgeFact::is_current));
1141
1142 if let Some(tf) = k
1143 .facts
1144 .iter_mut()
1145 .find(|f| f.category == "decision" && f.key == "session" && f.is_current())
1146 {
1147 tf.valid_until = Some(Utc::now());
1148 }
1149 k.judged_pairs.push(JudgedPair {
1150 key_a: "architecture/auth".into(),
1151 key_b: "decision/session".into(),
1152 verdict: "supersedes".into(),
1153 judged_at: Utc::now(),
1154 });
1155
1156 let cur: Vec<_> = k.facts.iter().filter(|f| f.is_current()).collect();
1157 assert_eq!(cur.len(), 1);
1158 assert_eq!(cur[0].category, "architecture");
1159 }
1160}