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