1use chrono::{DateTime, Utc};
49use regex::Regex;
50use serde::{Deserialize, Serialize};
51use std::collections::HashMap;
52use std::path::Path;
53use uuid::Uuid;
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct CaseKnowledge {
61 pub entry_id: Uuid,
63
64 pub case_context: String,
66
67 pub relevance_decay: f32,
70
71 pub explicit_links: Vec<Uuid>,
73
74 pub access_pattern: AccessPattern,
76
77 pub last_accessed: DateTime<Utc>,
79}
80
81impl CaseKnowledge {
82 pub fn new(entry_id: Uuid, case_context: impl Into<String>) -> Self {
84 Self {
85 entry_id,
86 case_context: case_context.into(),
87 relevance_decay: 1.0,
88 explicit_links: Vec::new(),
89 access_pattern: AccessPattern::ActiveUse,
90 last_accessed: Utc::now(),
91 }
92 }
93
94 pub fn with_access_pattern(mut self, pattern: AccessPattern) -> Self {
96 self.access_pattern = pattern;
97 self
98 }
99
100 pub fn with_link(mut self, linked_id: Uuid) -> Self {
102 self.explicit_links.push(linked_id);
103 self
104 }
105
106 pub fn update_decay(&mut self, half_life_days: f32) {
110 let days_since = (Utc::now() - self.last_accessed).num_hours() as f32 / 24.0;
111 self.relevance_decay = (-days_since / half_life_days).exp();
112 }
113
114 pub fn record_access(&mut self) {
116 self.last_accessed = Utc::now();
117 self.relevance_decay = 1.0;
118 }
119}
120
121#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
123pub enum AccessPattern {
124 ActiveUse,
126
127 #[default]
129 RecentHistory,
130
131 Archived,
133}
134
135impl AccessPattern {
136 pub fn boost_factor(&self) -> f32 {
138 match self {
139 AccessPattern::ActiveUse => 2.0,
140 AccessPattern::RecentHistory => 1.5,
141 AccessPattern::Archived => 0.5,
142 }
143 }
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct BackgroundKnowledge {
152 pub entry_id: Uuid,
154
155 pub domain: String,
157
158 pub permanence: Permanence,
160
161 pub supports: Vec<Uuid>,
163
164 pub last_verified: DateTime<Utc>,
166
167 pub version: Option<String>,
169}
170
171impl BackgroundKnowledge {
172 pub fn new(entry_id: Uuid, domain: impl Into<String>) -> Self {
174 Self {
175 entry_id,
176 domain: domain.into(),
177 permanence: Permanence::Evergreen,
178 supports: Vec::new(),
179 last_verified: Utc::now(),
180 version: None,
181 }
182 }
183
184 pub fn with_permanence(mut self, permanence: Permanence) -> Self {
186 self.permanence = permanence;
187 self
188 }
189
190 pub fn with_version(mut self, version: impl Into<String>) -> Self {
192 self.version = Some(version.into());
193 self
194 }
195
196 pub fn with_support(mut self, case_id: Uuid) -> Self {
198 self.supports.push(case_id);
199 self
200 }
201
202 pub fn is_valid(&self) -> bool {
204 !matches!(self.permanence, Permanence::Deprecated)
205 }
206}
207
208#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
210pub enum Permanence {
211 #[default]
213 Evergreen,
214
215 Versioned,
217
218 Temporal,
220
221 Deprecated,
223}
224
225impl Permanence {
226 pub fn support_factor(&self) -> f32 {
228 match self {
229 Permanence::Evergreen => 1.0,
230 Permanence::Versioned => 0.8,
231 Permanence::Temporal => 0.5,
232 Permanence::Deprecated => 0.1,
233 }
234 }
235}
236
237#[derive(Debug, Clone, Serialize, Deserialize)]
239pub enum KnowledgeType {
240 Case(CaseKnowledge),
242
243 Background(BackgroundKnowledge),
245}
246
247impl KnowledgeType {
248 pub fn entry_id(&self) -> Uuid {
250 match self {
251 KnowledgeType::Case(k) => k.entry_id,
252 KnowledgeType::Background(k) => k.entry_id,
253 }
254 }
255
256 pub fn is_case(&self) -> bool {
258 matches!(self, KnowledgeType::Case(_))
259 }
260
261 pub fn is_background(&self) -> bool {
263 matches!(self, KnowledgeType::Background(_))
264 }
265
266 pub fn as_case(&self) -> Option<&CaseKnowledge> {
268 match self {
269 KnowledgeType::Case(k) => Some(k),
270 KnowledgeType::Background(_) => None,
271 }
272 }
273
274 pub fn as_background(&self) -> Option<&BackgroundKnowledge> {
276 match self {
277 KnowledgeType::Case(_) => None,
278 KnowledgeType::Background(k) => Some(k),
279 }
280 }
281}
282
283#[derive(Debug, Clone, Serialize, Deserialize)]
285pub enum KnowledgeTypeHint {
286 Case {
288 context: String,
290 access_pattern: AccessPattern,
292 },
293
294 Background {
296 domain: String,
298 permanence: Permanence,
300 },
301}
302
303impl Default for KnowledgeTypeHint {
304 fn default() -> Self {
305 Self::Background {
306 domain: "general".to_string(),
307 permanence: Permanence::Evergreen,
308 }
309 }
310}
311
312#[derive(Debug, Clone, Serialize, Deserialize)]
314pub enum RoutingCondition {
315 SourcePath(String),
317
318 Category(String),
320
321 Tag(String),
323
324 ContentMatch(String),
326
327 Metadata(String, String),
329
330 All(Vec<RoutingCondition>),
332
333 Any(Vec<RoutingCondition>),
335}
336
337impl RoutingCondition {
338 pub fn matches(
340 &self,
341 source_path: &Path,
342 content: &str,
343 metadata: &HashMap<String, String>,
344 ) -> bool {
345 match self {
346 RoutingCondition::SourcePath(pattern) => {
347 glob_match(pattern, &source_path.to_string_lossy())
348 }
349 RoutingCondition::Category(category) => {
350 metadata.get("category").is_some_and(|c| c == category)
351 }
352 RoutingCondition::Tag(tag) => metadata
353 .get("tags")
354 .is_some_and(|tags| tags.split(',').any(|t| t.trim() == tag)),
355 RoutingCondition::ContentMatch(pattern) => {
356 Regex::new(pattern).is_ok_and(|re| re.is_match(content))
357 }
358 RoutingCondition::Metadata(key, value) => metadata.get(key).is_some_and(|v| v == value),
359 RoutingCondition::All(conditions) => conditions
360 .iter()
361 .all(|c| c.matches(source_path, content, metadata)),
362 RoutingCondition::Any(conditions) => conditions
363 .iter()
364 .any(|c| c.matches(source_path, content, metadata)),
365 }
366 }
367}
368
369fn glob_match(pattern: &str, path: &str) -> bool {
373 let regex_pattern = pattern
374 .replace('.', r"\.")
375 .replace("**/", "\x00")
377 .replace("**", "\x01")
379 .replace('*', "[^/]*")
380 .replace('\x00', "([^/]+/)*")
382 .replace('\x01', ".*");
384
385 Regex::new(&format!("^{regex_pattern}$")).is_ok_and(|re| re.is_match(path))
386}
387
388#[derive(Debug, Clone, Serialize, Deserialize)]
390pub struct RoutingRule {
391 pub condition: RoutingCondition,
393
394 pub knowledge_type: KnowledgeTypeHint,
396}
397
398impl RoutingRule {
399 pub fn new(condition: RoutingCondition, knowledge_type: KnowledgeTypeHint) -> Self {
401 Self {
402 condition,
403 knowledge_type,
404 }
405 }
406}
407
408#[derive(Debug, Clone)]
413pub struct KnowledgeRouter {
414 rules: Vec<RoutingRule>,
416
417 default_type: KnowledgeTypeHint,
419
420 scoring_weights: ScoringWeights,
422}
423
424#[derive(Debug, Clone, Serialize, Deserialize)]
426pub struct ScoringWeights {
427 pub source_weight: f32,
429
430 pub context_boost: f32,
432
433 pub support_factor: f32,
435
436 pub decay_half_life_days: f32,
438}
439
440impl Default for ScoringWeights {
441 fn default() -> Self {
442 Self {
443 source_weight: 1.0,
444 context_boost: 2.0,
445 support_factor: 0.5,
446 decay_half_life_days: 7.0,
447 }
448 }
449}
450
451impl KnowledgeRouter {
452 pub fn new() -> Self {
454 Self {
455 rules: Vec::new(),
456 default_type: KnowledgeTypeHint::default(),
457 scoring_weights: ScoringWeights::default(),
458 }
459 }
460
461 pub fn with_default(default_type: KnowledgeTypeHint) -> Self {
463 Self {
464 rules: Vec::new(),
465 default_type,
466 scoring_weights: ScoringWeights::default(),
467 }
468 }
469
470 pub fn with_scoring_weights(mut self, weights: ScoringWeights) -> Self {
472 self.scoring_weights = weights;
473 self
474 }
475
476 pub fn add_rule(&mut self, rule: RoutingRule) {
480 self.rules.push(rule);
481 }
482
483 pub fn add_rules(&mut self, rules: impl IntoIterator<Item = RoutingRule>) {
485 self.rules.extend(rules);
486 }
487
488 pub fn classify(
493 &self,
494 source_path: &Path,
495 content: &str,
496 metadata: &HashMap<String, String>,
497 ) -> KnowledgeType {
498 let entry_id = Uuid::new_v4();
499
500 let hint = self
501 .rules
502 .iter()
503 .find(|rule| rule.condition.matches(source_path, content, metadata))
504 .map(|rule| &rule.knowledge_type)
505 .unwrap_or(&self.default_type);
506
507 self.create_knowledge(entry_id, hint)
508 }
509
510 pub fn classify_with_id(
512 &self,
513 entry_id: Uuid,
514 source_path: &Path,
515 content: &str,
516 metadata: &HashMap<String, String>,
517 ) -> KnowledgeType {
518 let hint = self
519 .rules
520 .iter()
521 .find(|rule| rule.condition.matches(source_path, content, metadata))
522 .map(|rule| &rule.knowledge_type)
523 .unwrap_or(&self.default_type);
524
525 self.create_knowledge(entry_id, hint)
526 }
527
528 fn create_knowledge(&self, entry_id: Uuid, hint: &KnowledgeTypeHint) -> KnowledgeType {
530 match hint {
531 KnowledgeTypeHint::Case {
532 context,
533 access_pattern,
534 } => KnowledgeType::Case(
535 CaseKnowledge::new(entry_id, context).with_access_pattern(*access_pattern),
536 ),
537 KnowledgeTypeHint::Background { domain, permanence } => KnowledgeType::Background(
538 BackgroundKnowledge::new(entry_id, domain).with_permanence(*permanence),
539 ),
540 }
541 }
542
543 pub fn compute_relevance_score(
565 &self,
566 knowledge: &KnowledgeType,
567 base_similarity: f32,
568 query_context: Option<&str>,
569 ) -> f32 {
570 let weights = &self.scoring_weights;
571
572 match knowledge {
573 KnowledgeType::Case(case) => {
574 let context_matches = query_context.is_some_and(|ctx| ctx == case.case_context);
576
577 let context_multiplier = if context_matches {
578 weights.context_boost
579 } else {
580 1.0
581 };
582
583 let access_boost = case.access_pattern.boost_factor();
584
585 base_similarity * weights.source_weight
587 + context_multiplier * access_boost
588 + case.relevance_decay * access_boost
589 }
590 KnowledgeType::Background(bg) => {
591 let permanence_factor = bg.permanence.support_factor();
593
594 let support_bonus = if !bg.supports.is_empty() {
596 0.2 * bg.supports.len() as f32
597 } else {
598 0.0
599 };
600
601 base_similarity * weights.source_weight
603 + permanence_factor * weights.support_factor
604 + support_bonus
605 }
606 }
607 }
608
609 pub fn rules(&self) -> &[RoutingRule] {
611 &self.rules
612 }
613
614 pub fn default_type(&self) -> &KnowledgeTypeHint {
616 &self.default_type
617 }
618
619 pub fn scoring_weights(&self) -> &ScoringWeights {
621 &self.scoring_weights
622 }
623}
624
625impl Default for KnowledgeRouter {
626 fn default() -> Self {
627 Self::new()
628 }
629}
630
631#[cfg(test)]
632mod tests {
633 use super::*;
634
635 #[test]
636 fn test_case_knowledge_creation() {
637 let id = Uuid::new_v4();
638 let case = CaseKnowledge::new(id, "my-project")
639 .with_access_pattern(AccessPattern::ActiveUse)
640 .with_link(Uuid::new_v4());
641
642 assert_eq!(case.entry_id, id);
643 assert_eq!(case.case_context, "my-project");
644 assert_eq!(case.access_pattern, AccessPattern::ActiveUse);
645 assert_eq!(case.explicit_links.len(), 1);
646 assert!((case.relevance_decay - 1.0).abs() < f32::EPSILON);
647 }
648
649 #[test]
650 fn test_case_knowledge_decay() {
651 let id = Uuid::new_v4();
652 let mut case = CaseKnowledge::new(id, "test");
653
654 case.last_accessed = Utc::now() - chrono::Duration::days(7);
656 case.update_decay(7.0);
657
658 assert!(case.relevance_decay < 0.5);
660 assert!(case.relevance_decay > 0.3);
661 }
662
663 #[test]
664 fn test_background_knowledge_creation() {
665 let id = Uuid::new_v4();
666 let bg = BackgroundKnowledge::new(id, "rust")
667 .with_permanence(Permanence::Versioned)
668 .with_version("1.75")
669 .with_support(Uuid::new_v4());
670
671 assert_eq!(bg.entry_id, id);
672 assert_eq!(bg.domain, "rust");
673 assert_eq!(bg.permanence, Permanence::Versioned);
674 assert_eq!(bg.version, Some("1.75".to_string()));
675 assert_eq!(bg.supports.len(), 1);
676 assert!(bg.is_valid());
677 }
678
679 #[test]
680 fn test_background_knowledge_deprecated() {
681 let id = Uuid::new_v4();
682 let bg = BackgroundKnowledge::new(id, "legacy").with_permanence(Permanence::Deprecated);
683
684 assert!(!bg.is_valid());
685 }
686
687 #[test]
688 fn test_knowledge_type_accessors() {
689 let case_id = Uuid::new_v4();
690 let bg_id = Uuid::new_v4();
691
692 let case = KnowledgeType::Case(CaseKnowledge::new(case_id, "test"));
693 let bg = KnowledgeType::Background(BackgroundKnowledge::new(bg_id, "test"));
694
695 assert!(case.is_case());
696 assert!(!case.is_background());
697 assert_eq!(case.entry_id(), case_id);
698 assert!(case.as_case().is_some());
699 assert!(case.as_background().is_none());
700
701 assert!(!bg.is_case());
702 assert!(bg.is_background());
703 assert_eq!(bg.entry_id(), bg_id);
704 assert!(bg.as_case().is_none());
705 assert!(bg.as_background().is_some());
706 }
707
708 #[test]
709 fn test_access_pattern_boost() {
710 assert!((AccessPattern::ActiveUse.boost_factor() - 2.0).abs() < f32::EPSILON);
711 assert!((AccessPattern::RecentHistory.boost_factor() - 1.5).abs() < f32::EPSILON);
712 assert!((AccessPattern::Archived.boost_factor() - 0.5).abs() < f32::EPSILON);
713 }
714
715 #[test]
716 fn test_permanence_support_factor() {
717 assert!((Permanence::Evergreen.support_factor() - 1.0).abs() < f32::EPSILON);
718 assert!((Permanence::Versioned.support_factor() - 0.8).abs() < f32::EPSILON);
719 assert!((Permanence::Temporal.support_factor() - 0.5).abs() < f32::EPSILON);
720 assert!((Permanence::Deprecated.support_factor() - 0.1).abs() < f32::EPSILON);
721 }
722
723 #[test]
724 fn test_glob_matching() {
725 assert!(glob_match("*.rs", "main.rs"));
726 assert!(!glob_match("*.rs", "main.txt"));
727 assert!(glob_match("src/*.rs", "src/lib.rs"));
728 assert!(!glob_match("src/*.rs", "src/foo/lib.rs"));
729 assert!(glob_match("src/**/*.rs", "src/foo/bar/lib.rs"));
730 assert!(glob_match("projects/**/*", "projects/my-app/README.md"));
731 }
732
733 #[test]
734 fn test_routing_condition_source_path() {
735 let condition = RoutingCondition::SourcePath("projects/**/*".to_string());
736 let metadata = HashMap::new();
737
738 assert!(condition.matches(Path::new("projects/my-app/src/main.rs"), "", &metadata));
739 assert!(!condition.matches(Path::new("docs/README.md"), "", &metadata));
740 }
741
742 #[test]
743 fn test_routing_condition_category() {
744 let condition = RoutingCondition::Category("reference".to_string());
745 let mut metadata = HashMap::new();
746
747 assert!(!condition.matches(Path::new("test.md"), "", &metadata));
748
749 metadata.insert("category".to_string(), "reference".to_string());
750 assert!(condition.matches(Path::new("test.md"), "", &metadata));
751 }
752
753 #[test]
754 fn test_routing_condition_tag() {
755 let condition = RoutingCondition::Tag("rust".to_string());
756 let mut metadata = HashMap::new();
757
758 assert!(!condition.matches(Path::new("test.md"), "", &metadata));
759
760 metadata.insert("tags".to_string(), "programming, rust, systems".to_string());
761 assert!(condition.matches(Path::new("test.md"), "", &metadata));
762 }
763
764 #[test]
765 fn test_routing_condition_content_match() {
766 let condition = RoutingCondition::ContentMatch(r"TODO:|FIXME:".to_string());
767 let metadata = HashMap::new();
768
769 assert!(condition.matches(Path::new("test.rs"), "// TODO: implement this", &metadata));
770 assert!(!condition.matches(Path::new("test.rs"), "// This is done", &metadata));
771 }
772
773 #[test]
774 fn test_routing_condition_metadata() {
775 let condition = RoutingCondition::Metadata("status".to_string(), "active".to_string());
776 let mut metadata = HashMap::new();
777
778 assert!(!condition.matches(Path::new("test.md"), "", &metadata));
779
780 metadata.insert("status".to_string(), "active".to_string());
781 assert!(condition.matches(Path::new("test.md"), "", &metadata));
782 }
783
784 #[test]
785 fn test_routing_condition_all() {
786 let condition = RoutingCondition::All(vec![
787 RoutingCondition::SourcePath("src/**/*.rs".to_string()),
788 RoutingCondition::Category("code".to_string()),
789 ]);
790
791 let mut metadata = HashMap::new();
792 metadata.insert("category".to_string(), "code".to_string());
793
794 assert!(condition.matches(Path::new("src/lib.rs"), "", &metadata));
795 assert!(!condition.matches(Path::new("docs/README.md"), "", &metadata));
796
797 let mut wrong_category = HashMap::new();
798 wrong_category.insert("category".to_string(), "docs".to_string());
799 assert!(!condition.matches(Path::new("src/lib.rs"), "", &wrong_category));
800 }
801
802 #[test]
803 fn test_routing_condition_any() {
804 let condition = RoutingCondition::Any(vec![
805 RoutingCondition::Tag("important".to_string()),
806 RoutingCondition::Tag("urgent".to_string()),
807 ]);
808
809 let mut metadata1 = HashMap::new();
810 metadata1.insert("tags".to_string(), "important".to_string());
811 assert!(condition.matches(Path::new("test.md"), "", &metadata1));
812
813 let mut metadata2 = HashMap::new();
814 metadata2.insert("tags".to_string(), "urgent".to_string());
815 assert!(condition.matches(Path::new("test.md"), "", &metadata2));
816
817 let metadata3 = HashMap::new();
818 assert!(!condition.matches(Path::new("test.md"), "", &metadata3));
819 }
820
821 #[test]
822 fn test_router_classify_with_rule() {
823 let mut router = KnowledgeRouter::new();
824
825 router.add_rule(RoutingRule {
826 condition: RoutingCondition::SourcePath("projects/**/*".to_string()),
827 knowledge_type: KnowledgeTypeHint::Case {
828 context: "active-project".to_string(),
829 access_pattern: AccessPattern::ActiveUse,
830 },
831 });
832
833 let metadata = HashMap::new();
834 let knowledge = router.classify(
835 Path::new("projects/my-app/README.md"),
836 "Project documentation",
837 &metadata,
838 );
839
840 assert!(knowledge.is_case());
841 let case = knowledge.as_case().unwrap();
842 assert_eq!(case.case_context, "active-project");
843 assert_eq!(case.access_pattern, AccessPattern::ActiveUse);
844 }
845
846 #[test]
847 fn test_router_classify_default() {
848 let router = KnowledgeRouter::with_default(KnowledgeTypeHint::Background {
849 domain: "general".to_string(),
850 permanence: Permanence::Evergreen,
851 });
852
853 let metadata = HashMap::new();
854 let knowledge =
855 router.classify(Path::new("some/random/file.txt"), "Some content", &metadata);
856
857 assert!(knowledge.is_background());
858 let bg = knowledge.as_background().unwrap();
859 assert_eq!(bg.domain, "general");
860 assert_eq!(bg.permanence, Permanence::Evergreen);
861 }
862
863 #[test]
864 fn test_router_rule_priority() {
865 let mut router = KnowledgeRouter::new();
866
867 router.add_rule(RoutingRule {
869 condition: RoutingCondition::SourcePath("projects/urgent/**/*".to_string()),
870 knowledge_type: KnowledgeTypeHint::Case {
871 context: "urgent".to_string(),
872 access_pattern: AccessPattern::ActiveUse,
873 },
874 });
875
876 router.add_rule(RoutingRule {
878 condition: RoutingCondition::SourcePath("projects/**/*".to_string()),
879 knowledge_type: KnowledgeTypeHint::Case {
880 context: "normal".to_string(),
881 access_pattern: AccessPattern::RecentHistory,
882 },
883 });
884
885 let metadata = HashMap::new();
886
887 let urgent = router.classify(Path::new("projects/urgent/fix.rs"), "", &metadata);
889 assert_eq!(urgent.as_case().unwrap().case_context, "urgent");
890
891 let normal = router.classify(Path::new("projects/other/main.rs"), "", &metadata);
893 assert_eq!(normal.as_case().unwrap().case_context, "normal");
894 }
895
896 #[test]
897 fn test_compute_relevance_score_case_matching_context() {
898 let router = KnowledgeRouter::new();
899
900 let case = KnowledgeType::Case(
901 CaseKnowledge::new(Uuid::new_v4(), "my-project")
902 .with_access_pattern(AccessPattern::ActiveUse),
903 );
904
905 let score_match = router.compute_relevance_score(&case, 0.8, Some("my-project"));
907 let score_no_match = router.compute_relevance_score(&case, 0.8, Some("other-project"));
908 let score_no_context = router.compute_relevance_score(&case, 0.8, None);
909
910 assert!(score_match > score_no_match);
911 assert!(score_match > score_no_context);
912 }
913
914 #[test]
915 fn test_compute_relevance_score_background() {
916 let router = KnowledgeRouter::new();
917
918 let bg = KnowledgeType::Background(
919 BackgroundKnowledge::new(Uuid::new_v4(), "rust").with_permanence(Permanence::Evergreen),
920 );
921
922 let score = router.compute_relevance_score(&bg, 0.8, Some("any-context"));
923
924 assert!(score > 0.8);
926 }
927
928 #[test]
929 fn test_compute_relevance_score_background_with_supports() {
930 let router = KnowledgeRouter::new();
931
932 let bg_no_supports =
933 KnowledgeType::Background(BackgroundKnowledge::new(Uuid::new_v4(), "rust"));
934
935 let bg_with_supports = KnowledgeType::Background(
936 BackgroundKnowledge::new(Uuid::new_v4(), "rust")
937 .with_support(Uuid::new_v4())
938 .with_support(Uuid::new_v4()),
939 );
940
941 let score_no = router.compute_relevance_score(&bg_no_supports, 0.8, None);
942 let score_with = router.compute_relevance_score(&bg_with_supports, 0.8, None);
943
944 assert!(score_with > score_no);
946 }
947
948 #[test]
949 fn test_router_with_custom_weights() {
950 let weights = ScoringWeights {
951 source_weight: 2.0,
952 context_boost: 3.0,
953 support_factor: 0.8,
954 decay_half_life_days: 14.0,
955 };
956
957 let router = KnowledgeRouter::new().with_scoring_weights(weights);
958
959 assert!((router.scoring_weights().source_weight - 2.0).abs() < f32::EPSILON);
960 assert!((router.scoring_weights().context_boost - 3.0).abs() < f32::EPSILON);
961 }
962
963 #[test]
964 fn test_complex_routing_scenario() {
965 let mut router = KnowledgeRouter::new();
966
967 router.add_rule(RoutingRule::new(
969 RoutingCondition::All(vec![
970 RoutingCondition::SourcePath("src/**/*.rs".to_string()),
971 RoutingCondition::Metadata("status".to_string(), "active".to_string()),
972 ]),
973 KnowledgeTypeHint::Case {
974 context: "current-sprint".to_string(),
975 access_pattern: AccessPattern::ActiveUse,
976 },
977 ));
978
979 router.add_rule(RoutingRule::new(
981 RoutingCondition::Any(vec![
982 RoutingCondition::Category("reference".to_string()),
983 RoutingCondition::Tag("documentation".to_string()),
984 ]),
985 KnowledgeTypeHint::Background {
986 domain: "documentation".to_string(),
987 permanence: Permanence::Versioned,
988 },
989 ));
990
991 let mut metadata1 = HashMap::new();
993 metadata1.insert("status".to_string(), "active".to_string());
994
995 let code = router.classify(Path::new("src/lib.rs"), "pub fn main() {}", &metadata1);
996 assert!(code.is_case());
997 assert_eq!(code.as_case().unwrap().case_context, "current-sprint");
998
999 let mut metadata2 = HashMap::new();
1001 metadata2.insert("category".to_string(), "reference".to_string());
1002
1003 let doc = router.classify(Path::new("docs/api.md"), "# API Reference", &metadata2);
1004 assert!(doc.is_background());
1005 assert_eq!(doc.as_background().unwrap().domain, "documentation");
1006
1007 let other = router.classify(Path::new("random.txt"), "some content", &HashMap::new());
1009 assert!(other.is_background()); }
1011}