1use std::collections::{HashMap, VecDeque};
8
9pub type SvtVersionId = u64;
15
16#[derive(Debug, Clone, PartialEq)]
22pub enum SvtError {
23 VersionNotFound(SvtVersionId),
25 AnchorNotFound {
27 concept: String,
28 version_id: SvtVersionId,
29 },
30 DimMismatch { expected: usize, got: usize },
32 InsufficientAnchors { found: usize, required: usize },
34 VersionDeprecated(SvtVersionId),
36 InvalidConcept,
38}
39
40impl std::fmt::Display for SvtError {
41 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42 match self {
43 SvtError::VersionNotFound(id) => write!(f, "version {id} not found"),
44 SvtError::AnchorNotFound {
45 concept,
46 version_id,
47 } => {
48 write!(f, "anchor '{concept}' not found for version {version_id}")
49 }
50 SvtError::DimMismatch { expected, got } => {
51 write!(f, "dimension mismatch: expected {expected}, got {got}")
52 }
53 SvtError::InsufficientAnchors { found, required } => {
54 write!(
55 f,
56 "insufficient anchors: {found} found, {required} required"
57 )
58 }
59 SvtError::VersionDeprecated(id) => write!(f, "version {id} is deprecated"),
60 SvtError::InvalidConcept => write!(f, "concept name must not be empty"),
61 }
62 }
63}
64
65impl std::error::Error for SvtError {}
66
67#[derive(Debug, Clone)]
73pub struct SvtTrackerConfig {
74 pub drift_threshold: f64,
76 pub min_anchors: usize,
78 pub window_size: usize,
81 pub auto_deprecate: bool,
85}
86
87impl Default for SvtTrackerConfig {
88 fn default() -> Self {
89 Self {
90 drift_threshold: 0.15,
91 min_anchors: 1,
92 window_size: 20,
93 auto_deprecate: false,
94 }
95 }
96}
97
98#[derive(Debug, Clone)]
104pub struct SvtVersion {
105 pub id: SvtVersionId,
107 pub name: String,
109 pub created_at: u64,
111 pub is_active: bool,
113 pub embedding_dim: usize,
115 pub anchor_count: u32,
117}
118
119#[derive(Debug, Clone)]
125pub struct SvtDriftEvent {
126 pub ts: u64,
128 pub version_a: SvtVersionId,
130 pub version_b: SvtVersionId,
132 pub concept: String,
134 pub drift_score: f64,
136 pub is_significant: bool,
138}
139
140#[derive(Debug, Clone)]
146pub struct SvtDriftReport {
147 pub version_a: SvtVersionId,
149 pub version_b: SvtVersionId,
151 pub overall_drift: f64,
153 pub drifted_concepts: Vec<(String, f64)>,
156 pub stable_concepts: Vec<String>,
158 pub recommendation: String,
160}
161
162#[derive(Debug, Clone)]
168pub struct SvtTrackerStats {
169 pub total_versions: usize,
171 pub active_versions: usize,
173 pub total_anchors: usize,
175 pub distinct_concepts: usize,
177 pub drift_events: usize,
179 pub mean_logged_drift: f64,
181 pub most_stable_concept: Option<String>,
183 pub most_drifted_concept: Option<String>,
185}
186
187#[inline]
194fn cosine_similarity(a: &[f64], b: &[f64]) -> f64 {
195 let dot: f64 = a.iter().zip(b).map(|(x, y)| x * y).sum();
196 let na = a.iter().map(|x| x * x).sum::<f64>().sqrt();
197 let nb = b.iter().map(|x| x * x).sum::<f64>().sqrt();
198 if na == 0.0 || nb == 0.0 {
199 0.0
200 } else {
201 dot / (na * nb)
202 }
203}
204
205#[inline]
207fn cosine_distance(a: &[f64], b: &[f64]) -> f64 {
208 (1.0 - cosine_similarity(a, b)).clamp(0.0, 1.0)
209}
210
211#[inline]
214fn xorshift64(state: &mut u64) -> u64 {
215 let mut x = *state;
216 x ^= x << 13;
217 x ^= x >> 7;
218 x ^= x << 17;
219 *state = x;
220 x
221}
222
223const DRIFT_LOG_CAP: usize = 500;
228
229#[derive(Debug)]
263pub struct SemanticVersioningTracker {
264 versions: HashMap<SvtVersionId, SvtVersion>,
266 anchors: HashMap<String, Vec<(SvtVersionId, Vec<f64>)>>,
268 drift_log: VecDeque<SvtDriftEvent>,
270 config: SvtTrackerConfig,
272 next_id: SvtVersionId,
274 rng_state: u64,
276}
277
278pub type SvtSemanticVersioningTracker = SemanticVersioningTracker;
280
281impl SemanticVersioningTracker {
282 pub fn new(config: SvtTrackerConfig) -> Self {
288 Self {
289 versions: HashMap::new(),
290 anchors: HashMap::new(),
291 drift_log: VecDeque::with_capacity(DRIFT_LOG_CAP + 1),
292 config,
293 next_id: 1,
294 rng_state: 0x5851_F42D_4C95_7F2D,
295 }
296 }
297
298 fn now_ts(&mut self) -> u64 {
302 use std::time::{SystemTime, UNIX_EPOCH};
303 SystemTime::now()
304 .duration_since(UNIX_EPOCH)
305 .map(|d| d.as_secs())
306 .unwrap_or_else(|_| xorshift64(&mut self.rng_state))
307 }
308
309 pub fn with_defaults() -> Self {
311 Self::new(SvtTrackerConfig::default())
312 }
313
314 pub fn register_version(
324 &mut self,
325 name: impl Into<String>,
326 dim: usize,
327 ) -> Result<SvtVersionId, SvtError> {
328 if dim == 0 {
329 return Err(SvtError::DimMismatch {
330 expected: 1,
331 got: 0,
332 });
333 }
334 let id = self.next_id;
335 self.next_id = self.next_id.wrapping_add(1);
336 let version = SvtVersion {
337 id,
338 name: name.into(),
339 created_at: self.now_ts(),
340 is_active: true,
341 embedding_dim: dim,
342 anchor_count: 0,
343 };
344 self.versions.insert(id, version);
345 Ok(id)
346 }
347
348 pub fn deprecate_version(&mut self, id: SvtVersionId) -> Result<(), SvtError> {
354 let ver = self
355 .versions
356 .get_mut(&id)
357 .ok_or(SvtError::VersionNotFound(id))?;
358 ver.is_active = false;
359 Ok(())
360 }
361
362 pub fn activate_version(&mut self, id: SvtVersionId) -> Result<(), SvtError> {
368 let ver = self
369 .versions
370 .get_mut(&id)
371 .ok_or(SvtError::VersionNotFound(id))?;
372 ver.is_active = true;
373 Ok(())
374 }
375
376 pub fn get_version(&self, id: SvtVersionId) -> Result<&SvtVersion, SvtError> {
382 self.versions.get(&id).ok_or(SvtError::VersionNotFound(id))
383 }
384
385 pub fn list_versions(&self) -> Vec<&SvtVersion> {
387 let mut v: Vec<&SvtVersion> = self.versions.values().collect();
388 v.sort_by_key(|ver| ver.id);
389 v
390 }
391
392 pub fn active_versions(&self) -> Vec<&SvtVersion> {
394 let mut v: Vec<&SvtVersion> = self.versions.values().filter(|ver| ver.is_active).collect();
395 v.sort_by_key(|ver| ver.id);
396 v
397 }
398
399 pub fn add_anchor(
415 &mut self,
416 concept: &str,
417 version_id: SvtVersionId,
418 embedding: Vec<f64>,
419 ) -> Result<(), SvtError> {
420 if concept.is_empty() {
421 return Err(SvtError::InvalidConcept);
422 }
423 let ver = self
424 .versions
425 .get_mut(&version_id)
426 .ok_or(SvtError::VersionNotFound(version_id))?;
427
428 if embedding.len() != ver.embedding_dim {
429 return Err(SvtError::DimMismatch {
430 expected: ver.embedding_dim,
431 got: embedding.len(),
432 });
433 }
434
435 let entry = self.anchors.entry(concept.to_owned()).or_default();
436
437 if let Some(existing) = entry.iter_mut().find(|(vid, _)| *vid == version_id) {
439 existing.1 = embedding;
440 } else {
441 entry.push((version_id, embedding));
442 ver.anchor_count = ver.anchor_count.saturating_add(1);
443 }
444
445 Ok(())
446 }
447
448 pub fn get_anchor(&self, concept: &str, version_id: SvtVersionId) -> Result<&[f64], SvtError> {
454 let entries = self
455 .anchors
456 .get(concept)
457 .ok_or_else(|| SvtError::AnchorNotFound {
458 concept: concept.to_owned(),
459 version_id,
460 })?;
461 entries
462 .iter()
463 .find(|(vid, _)| *vid == version_id)
464 .map(|(_, emb)| emb.as_slice())
465 .ok_or_else(|| SvtError::AnchorNotFound {
466 concept: concept.to_owned(),
467 version_id,
468 })
469 }
470
471 pub fn concepts_for_version(&self, version_id: SvtVersionId) -> Vec<&str> {
473 self.anchors
474 .iter()
475 .filter_map(|(concept, entries)| {
476 if entries.iter().any(|(vid, _)| *vid == version_id) {
477 Some(concept.as_str())
478 } else {
479 None
480 }
481 })
482 .collect()
483 }
484
485 pub fn compute_drift(
500 &mut self,
501 ver_a: SvtVersionId,
502 ver_b: SvtVersionId,
503 ) -> Result<SvtDriftReport, SvtError> {
504 if !self.versions.contains_key(&ver_a) {
506 return Err(SvtError::VersionNotFound(ver_a));
507 }
508 if !self.versions.contains_key(&ver_b) {
509 return Err(SvtError::VersionNotFound(ver_b));
510 }
511
512 let shared: Vec<String> = self
514 .anchors
515 .iter()
516 .filter_map(|(concept, entries)| {
517 let has_a = entries.iter().any(|(vid, _)| *vid == ver_a);
518 let has_b = entries.iter().any(|(vid, _)| *vid == ver_b);
519 if has_a && has_b {
520 Some(concept.clone())
521 } else {
522 None
523 }
524 })
525 .collect();
526
527 if shared.len() < self.config.min_anchors {
528 return Err(SvtError::InsufficientAnchors {
529 found: shared.len(),
530 required: self.config.min_anchors,
531 });
532 }
533
534 let ts = self.now_ts();
535 let threshold = self.config.drift_threshold;
536 let mut concept_scores: Vec<(String, f64)> = Vec::with_capacity(shared.len());
537
538 for concept in &shared {
539 let entries = match self.anchors.get(concept) {
540 Some(e) => e,
541 None => continue,
542 };
543 let emb_a = match entries.iter().find(|(vid, _)| *vid == ver_a) {
544 Some((_, e)) => e.as_slice(),
545 None => continue,
546 };
547 let emb_b = match entries.iter().find(|(vid, _)| *vid == ver_b) {
548 Some((_, e)) => e.as_slice(),
549 None => continue,
550 };
551
552 let score = cosine_distance(emb_a, emb_b);
553 concept_scores.push((concept.clone(), score));
554
555 let event = SvtDriftEvent {
557 ts,
558 version_a: ver_a,
559 version_b: ver_b,
560 concept: concept.clone(),
561 drift_score: score,
562 is_significant: score >= threshold,
563 };
564 self.push_drift_event(event);
565 }
566
567 let overall_drift = if concept_scores.is_empty() {
569 0.0
570 } else {
571 concept_scores.iter().map(|(_, s)| s).sum::<f64>() / concept_scores.len() as f64
572 };
573
574 let mut drifted_concepts: Vec<(String, f64)> = concept_scores
576 .iter()
577 .filter(|(_, s)| *s >= threshold)
578 .cloned()
579 .collect();
580 drifted_concepts.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
581
582 let stable_concepts: Vec<String> = concept_scores
583 .iter()
584 .filter(|(_, s)| *s < threshold)
585 .map(|(c, _)| c.clone())
586 .collect();
587
588 let recommendation =
590 self.build_recommendation(ver_a, ver_b, overall_drift, &drifted_concepts);
591
592 if self.config.auto_deprecate && overall_drift >= threshold * 2.0 {
594 if let Some(ver) = self.versions.get_mut(&ver_a) {
595 ver.is_active = false;
596 }
597 }
598
599 Ok(SvtDriftReport {
600 version_a: ver_a,
601 version_b: ver_b,
602 overall_drift,
603 drifted_concepts,
604 stable_concepts,
605 recommendation,
606 })
607 }
608
609 pub fn find_drifted_concepts(
616 &self,
617 ver_a: SvtVersionId,
618 ver_b: SvtVersionId,
619 threshold: f64,
620 ) -> Result<Vec<(String, f64)>, SvtError> {
621 if !self.versions.contains_key(&ver_a) {
622 return Err(SvtError::VersionNotFound(ver_a));
623 }
624 if !self.versions.contains_key(&ver_b) {
625 return Err(SvtError::VersionNotFound(ver_b));
626 }
627
628 let mut result: Vec<(String, f64)> = self
629 .anchors
630 .iter()
631 .filter_map(|(concept, entries)| {
632 let emb_a = entries.iter().find(|(vid, _)| *vid == ver_a)?.1.as_slice();
633 let emb_b = entries.iter().find(|(vid, _)| *vid == ver_b)?.1.as_slice();
634 let score = cosine_distance(emb_a, emb_b);
635 if score >= threshold {
636 Some((concept.clone(), score))
637 } else {
638 None
639 }
640 })
641 .collect();
642
643 result.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
644 Ok(result)
645 }
646
647 pub fn semantic_similarity_over_time(
663 &self,
664 concept: &str,
665 ) -> Result<Vec<(SvtVersionId, SvtVersionId, f64)>, SvtError> {
666 if concept.is_empty() {
667 return Err(SvtError::InvalidConcept);
668 }
669
670 let entries = self
671 .anchors
672 .get(concept)
673 .ok_or(SvtError::InsufficientAnchors {
674 found: 0,
675 required: 2,
676 })?;
677
678 let mut sorted: Vec<(SvtVersionId, &[f64])> = entries
680 .iter()
681 .map(|(vid, emb)| (*vid, emb.as_slice()))
682 .collect();
683 sorted.sort_by_key(|(vid, _)| *vid);
684
685 if sorted.len() < 2 {
686 return Err(SvtError::InsufficientAnchors {
687 found: sorted.len(),
688 required: 2,
689 });
690 }
691
692 let window = self.config.window_size.max(1);
693 let pairs: Vec<(SvtVersionId, SvtVersionId, f64)> = sorted
694 .windows(2)
695 .take(window)
696 .map(|w| {
697 let (va, emb_a) = w[0];
698 let (vb, emb_b) = w[1];
699 let sim = cosine_similarity(emb_a, emb_b);
700 (va, vb, sim)
701 })
702 .collect();
703
704 Ok(pairs)
705 }
706
707 pub fn stability_score(&self, concept: &str) -> Result<f64, SvtError> {
718 if concept.is_empty() {
719 return Err(SvtError::InvalidConcept);
720 }
721
722 let entries = match self.anchors.get(concept) {
723 Some(e) => e,
724 None => return Ok(1.0),
725 };
726
727 let mut sorted: Vec<(SvtVersionId, &[f64])> = entries
728 .iter()
729 .map(|(vid, emb)| (*vid, emb.as_slice()))
730 .collect();
731 sorted.sort_by_key(|(vid, _)| *vid);
732
733 if sorted.len() < 2 {
734 return Ok(1.0);
735 }
736
737 let total_drift: f64 = sorted
738 .windows(2)
739 .map(|w| cosine_distance(w[0].1, w[1].1))
740 .sum();
741 let n = (sorted.len() - 1) as f64;
742 Ok((1.0 - total_drift / n).clamp(0.0, 1.0))
743 }
744
745 pub fn recommend_migration(
759 &self,
760 from: SvtVersionId,
761 to: SvtVersionId,
762 ) -> Result<Vec<String>, SvtError> {
763 let drifted = self.find_drifted_concepts(from, to, self.config.drift_threshold)?;
764 Ok(drifted.into_iter().map(|(c, _)| c).collect())
765 }
766
767 pub fn tracker_stats(&self) -> SvtTrackerStats {
773 let total_versions = self.versions.len();
774 let active_versions = self.versions.values().filter(|v| v.is_active).count();
775
776 let total_anchors: usize = self.anchors.values().map(|v| v.len()).sum();
777 let distinct_concepts = self.anchors.len();
778 let drift_events = self.drift_log.len();
779
780 let mean_logged_drift = if drift_events == 0 {
781 0.0
782 } else {
783 self.drift_log.iter().map(|e| e.drift_score).sum::<f64>() / drift_events as f64
784 };
785
786 let mut concept_drift_sums: HashMap<&str, (f64, usize)> = HashMap::new();
788 for event in &self.drift_log {
789 let entry = concept_drift_sums
790 .entry(event.concept.as_str())
791 .or_insert((0.0, 0));
792 entry.0 += event.drift_score;
793 entry.1 += 1;
794 }
795
796 let most_stable_concept = concept_drift_sums
797 .iter()
798 .min_by(|a, b| {
799 let avg_a = a.1 .0 / a.1 .1 as f64;
800 let avg_b = b.1 .0 / b.1 .1 as f64;
801 avg_a
802 .partial_cmp(&avg_b)
803 .unwrap_or(std::cmp::Ordering::Equal)
804 })
805 .map(|(c, _)| (*c).to_owned());
806
807 let most_drifted_concept = concept_drift_sums
808 .iter()
809 .max_by(|a, b| {
810 let avg_a = a.1 .0 / a.1 .1 as f64;
811 let avg_b = b.1 .0 / b.1 .1 as f64;
812 avg_a
813 .partial_cmp(&avg_b)
814 .unwrap_or(std::cmp::Ordering::Equal)
815 })
816 .map(|(c, _)| (*c).to_owned());
817
818 SvtTrackerStats {
819 total_versions,
820 active_versions,
821 total_anchors,
822 distinct_concepts,
823 drift_events,
824 mean_logged_drift,
825 most_stable_concept,
826 most_drifted_concept,
827 }
828 }
829
830 pub fn drift_log(&self) -> &VecDeque<SvtDriftEvent> {
832 &self.drift_log
833 }
834
835 pub fn clear_drift_log(&mut self) {
837 self.drift_log.clear();
838 }
839
840 pub fn config(&self) -> &SvtTrackerConfig {
842 &self.config
843 }
844
845 pub fn config_mut(&mut self) -> &mut SvtTrackerConfig {
847 &mut self.config
848 }
849
850 fn push_drift_event(&mut self, event: SvtDriftEvent) {
857 if self.drift_log.len() >= DRIFT_LOG_CAP {
858 self.drift_log.pop_front();
859 }
860 self.drift_log.push_back(event);
861 }
862
863 fn build_recommendation(
865 &self,
866 ver_a: SvtVersionId,
867 ver_b: SvtVersionId,
868 overall_drift: f64,
869 drifted: &[(String, f64)],
870 ) -> String {
871 let threshold = self.config.drift_threshold;
872 if overall_drift < threshold * 0.5 {
873 format!(
874 "Versions {ver_a} → {ver_b} are semantically compatible \
875 (overall drift {overall_drift:.4} < {half:.4}). \
876 Migration should be transparent.",
877 half = threshold * 0.5
878 )
879 } else if overall_drift < threshold {
880 format!(
881 "Versions {ver_a} → {ver_b} show minor drift (overall {overall_drift:.4}). \
882 Spot-check {n} concept(s) before production rollout.",
883 n = drifted.len()
884 )
885 } else if overall_drift < threshold * 2.0 {
886 let top: Vec<&str> = drifted.iter().take(5).map(|(c, _)| c.as_str()).collect();
887 format!(
888 "Versions {ver_a} → {ver_b} show significant drift (overall {overall_drift:.4}). \
889 Re-evaluate embeddings for: {top}.",
890 top = top.join(", ")
891 )
892 } else {
893 let top: Vec<&str> = drifted.iter().take(10).map(|(c, _)| c.as_str()).collect();
894 format!(
895 "Versions {ver_a} → {ver_b} are semantically incompatible \
896 (overall drift {overall_drift:.4} >= {dbl:.4}). \
897 Full re-indexing recommended. Affected concepts: {top}.",
898 dbl = threshold * 2.0,
899 top = top.join(", ")
900 )
901 }
902 }
903
904 pub fn add_anchors_batch(
913 &mut self,
914 items: impl IntoIterator<Item = (String, SvtVersionId, Vec<f64>)>,
915 ) -> Vec<SvtError> {
916 let mut errors = Vec::new();
917 let batch: Vec<_> = items.into_iter().collect();
918 for (concept, version_id, embedding) in batch {
919 if let Err(e) = self.add_anchor(&concept, version_id, embedding) {
920 errors.push(e);
921 }
922 }
923 errors
924 }
925
926 pub fn compute_all_consecutive_drifts(&mut self) -> Vec<Result<SvtDriftReport, SvtError>> {
931 let ids: Vec<SvtVersionId> = {
932 let mut v: Vec<SvtVersionId> = self
933 .versions
934 .values()
935 .filter(|ver| ver.is_active)
936 .map(|ver| ver.id)
937 .collect();
938 v.sort_unstable();
939 v
940 };
941
942 let pairs: Vec<(SvtVersionId, SvtVersionId)> =
943 ids.windows(2).map(|w| (w[0], w[1])).collect();
944
945 pairs
946 .into_iter()
947 .map(|(a, b)| self.compute_drift(a, b))
948 .collect()
949 }
950
951 pub fn global_stability(&self) -> f64 {
955 if self.anchors.is_empty() {
956 return 1.0;
957 }
958 let total: f64 = self
959 .anchors
960 .keys()
961 .map(|c| self.stability_score(c).unwrap_or(1.0))
962 .sum();
963 total / self.anchors.len() as f64
964 }
965
966 pub fn concepts_by_stability(&self) -> Vec<(String, f64)> {
974 let mut scores: Vec<(String, f64)> = self
975 .anchors
976 .keys()
977 .map(|c| {
978 let s = self.stability_score(c).unwrap_or(1.0);
979 (c.clone(), s)
980 })
981 .collect();
982 scores.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
983 scores
984 }
985
986 pub fn top_drifted_concepts(
992 &self,
993 ver_a: SvtVersionId,
994 ver_b: SvtVersionId,
995 n: usize,
996 ) -> Result<Vec<(String, f64)>, SvtError> {
997 let mut all = self.find_drifted_concepts(ver_a, ver_b, 0.0)?;
998 all.truncate(n);
999 Ok(all)
1000 }
1001
1002 pub fn are_compatible(
1010 &mut self,
1011 ver_a: SvtVersionId,
1012 ver_b: SvtVersionId,
1013 ) -> Result<bool, SvtError> {
1014 let report = self.compute_drift(ver_a, ver_b)?;
1015 Ok(report.overall_drift < self.config.drift_threshold)
1016 }
1017
1018 pub fn remove_version_anchors(&mut self, version_id: SvtVersionId) -> Result<usize, SvtError> {
1024 if !self.versions.contains_key(&version_id) {
1025 return Err(SvtError::VersionNotFound(version_id));
1026 }
1027 let mut removed = 0usize;
1028 for entries in self.anchors.values_mut() {
1029 let before = entries.len();
1030 entries.retain(|(vid, _)| *vid != version_id);
1031 removed += before - entries.len();
1032 }
1033 self.anchors.retain(|_, entries| !entries.is_empty());
1035 if let Some(ver) = self.versions.get_mut(&version_id) {
1037 ver.anchor_count = 0;
1038 }
1039 Ok(removed)
1040 }
1041
1042 pub fn concept_drift_matrix(
1049 &self,
1050 ver_a: SvtVersionId,
1051 ver_b: SvtVersionId,
1052 ) -> Result<HashMap<String, f64>, SvtError> {
1053 if !self.versions.contains_key(&ver_a) {
1054 return Err(SvtError::VersionNotFound(ver_a));
1055 }
1056 if !self.versions.contains_key(&ver_b) {
1057 return Err(SvtError::VersionNotFound(ver_b));
1058 }
1059 let matrix: HashMap<String, f64> = self
1060 .anchors
1061 .iter()
1062 .filter_map(|(concept, entries)| {
1063 let emb_a = entries.iter().find(|(vid, _)| *vid == ver_a)?.1.as_slice();
1064 let emb_b = entries.iter().find(|(vid, _)| *vid == ver_b)?.1.as_slice();
1065 Some((concept.clone(), cosine_distance(emb_a, emb_b)))
1066 })
1067 .collect();
1068 Ok(matrix)
1069 }
1070}
1071
1072#[cfg(test)]
1077mod tests {
1078 use super::*;
1079
1080 fn tracker() -> SemanticVersioningTracker {
1083 SemanticVersioningTracker::with_defaults()
1084 }
1085
1086 fn tracker_with(threshold: f64) -> SemanticVersioningTracker {
1087 SemanticVersioningTracker::new(SvtTrackerConfig {
1088 drift_threshold: threshold,
1089 min_anchors: 1,
1090 window_size: 20,
1091 auto_deprecate: false,
1092 })
1093 }
1094
1095 fn unit(dim: usize, pos: usize) -> Vec<f64> {
1097 let mut v = vec![0.0f64; dim];
1098 v[pos] = 1.0;
1099 v
1100 }
1101
1102 fn near_unit(dim: usize, pos: usize, eps: f64) -> Vec<f64> {
1104 let mut v = vec![eps; dim];
1105 v[pos] = 1.0;
1106 v
1107 }
1108
1109 #[test]
1112 fn cosine_similarity_identical() {
1113 let v = vec![1.0, 2.0, 3.0];
1114 let s = cosine_similarity(&v, &v);
1115 assert!((s - 1.0).abs() < 1e-10);
1116 }
1117
1118 #[test]
1119 fn cosine_similarity_orthogonal() {
1120 let a = vec![1.0, 0.0];
1121 let b = vec![0.0, 1.0];
1122 let s = cosine_similarity(&a, &b);
1123 assert!(s.abs() < 1e-10);
1124 }
1125
1126 #[test]
1127 fn cosine_similarity_opposite() {
1128 let a = vec![1.0, 0.0];
1129 let b = vec![-1.0, 0.0];
1130 let s = cosine_similarity(&a, &b);
1131 assert!((s + 1.0).abs() < 1e-10);
1132 }
1133
1134 #[test]
1135 fn cosine_similarity_zero_vector() {
1136 let z = vec![0.0, 0.0];
1137 let a = vec![1.0, 0.0];
1138 assert_eq!(cosine_similarity(&z, &a), 0.0);
1139 assert_eq!(cosine_similarity(&a, &z), 0.0);
1140 assert_eq!(cosine_similarity(&z, &z), 0.0);
1141 }
1142
1143 #[test]
1144 fn cosine_distance_identical_is_zero() {
1145 let v = vec![1.0, 0.5, 0.5];
1146 assert!((cosine_distance(&v, &v)).abs() < 1e-10);
1147 }
1148
1149 #[test]
1150 fn cosine_distance_orthogonal_is_one() {
1151 let a = vec![1.0, 0.0];
1152 let b = vec![0.0, 1.0];
1153 assert!((cosine_distance(&a, &b) - 1.0).abs() < 1e-10);
1154 }
1155
1156 #[test]
1159 fn xorshift64_produces_nonzero() {
1160 let mut state: u64 = 42;
1161 let v = xorshift64(&mut state);
1162 assert_ne!(v, 0);
1163 assert_ne!(state, 42);
1164 }
1165
1166 #[test]
1167 fn xorshift64_sequence_different() {
1168 let mut state: u64 = 0xCAFE_BABE;
1169 let v1 = xorshift64(&mut state);
1170 let v2 = xorshift64(&mut state);
1171 assert_ne!(v1, v2);
1172 }
1173
1174 #[test]
1177 fn register_version_basic() {
1178 let mut t = tracker();
1179 let id = t
1180 .register_version("v1", 128)
1181 .expect("test: register version v1 with dim 128 should succeed");
1182 assert!(id > 0);
1183 let ver = t
1184 .get_version(id)
1185 .expect("test: get version by id should succeed for just-registered version");
1186 assert_eq!(ver.name, "v1");
1187 assert_eq!(ver.embedding_dim, 128);
1188 assert!(ver.is_active);
1189 assert_eq!(ver.anchor_count, 0);
1190 }
1191
1192 #[test]
1193 fn register_version_ids_monotone() {
1194 let mut t = tracker();
1195 let a = t
1196 .register_version("a", 16)
1197 .expect("test: register version a should succeed");
1198 let b = t
1199 .register_version("b", 16)
1200 .expect("test: register version b should succeed");
1201 let c = t
1202 .register_version("c", 16)
1203 .expect("test: register version c should succeed");
1204 assert!(a < b);
1205 assert!(b < c);
1206 }
1207
1208 #[test]
1209 fn register_version_zero_dim_err() {
1210 let mut t = tracker();
1211 let res = t.register_version("bad", 0);
1212 assert!(res.is_err());
1213 }
1214
1215 #[test]
1216 fn deprecate_and_activate() {
1217 let mut t = tracker();
1218 let id = t
1219 .register_version("v1", 8)
1220 .expect("test: register version v1 should succeed");
1221 t.deprecate_version(id)
1222 .expect("test: deprecate version should succeed for known id");
1223 assert!(
1224 !t.get_version(id)
1225 .expect("test: get version should succeed for known id")
1226 .is_active
1227 );
1228 t.activate_version(id)
1229 .expect("test: activate version should succeed for known id");
1230 assert!(
1231 t.get_version(id)
1232 .expect("test: get version should succeed for known id")
1233 .is_active
1234 );
1235 }
1236
1237 #[test]
1238 fn deprecate_nonexistent_err() {
1239 let mut t = tracker();
1240 assert!(t.deprecate_version(9999).is_err());
1241 }
1242
1243 #[test]
1244 fn activate_nonexistent_err() {
1245 let mut t = tracker();
1246 assert!(t.activate_version(9999).is_err());
1247 }
1248
1249 #[test]
1250 fn list_versions_sorted() {
1251 let mut t = tracker();
1252 let c = t
1253 .register_version("c", 4)
1254 .expect("test: register version c should succeed");
1255 let a = t
1256 .register_version("a", 4)
1257 .expect("test: register version a should succeed");
1258 let b = t
1259 .register_version("b", 4)
1260 .expect("test: register version b should succeed");
1261 let listed: Vec<SvtVersionId> = t.list_versions().iter().map(|v| v.id).collect();
1262 assert!(listed.contains(&a));
1263 assert!(listed.contains(&b));
1264 assert!(listed.contains(&c));
1265 for w in listed.windows(2) {
1267 assert!(w[0] < w[1]);
1268 }
1269 }
1270
1271 #[test]
1272 fn active_versions_filters_deprecated() {
1273 let mut t = tracker();
1274 let a = t
1275 .register_version("a", 4)
1276 .expect("test: register version a should succeed");
1277 let b = t
1278 .register_version("b", 4)
1279 .expect("test: register version b should succeed");
1280 t.deprecate_version(a)
1281 .expect("test: deprecate version a should succeed");
1282 let active_ids: Vec<SvtVersionId> = t.active_versions().iter().map(|v| v.id).collect();
1283 assert!(!active_ids.contains(&a));
1284 assert!(active_ids.contains(&b));
1285 }
1286
1287 #[test]
1290 fn add_anchor_basic() {
1291 let mut t = tracker();
1292 let v = t
1293 .register_version("v", 3)
1294 .expect("test: register version v should succeed");
1295 t.add_anchor("cat", v, unit(3, 0))
1296 .expect("test: add anchor cat to version v should succeed");
1297 let emb = t
1298 .get_anchor("cat", v)
1299 .expect("test: get anchor cat for version v should succeed");
1300 assert_eq!(emb, &unit(3, 0));
1301 }
1302
1303 #[test]
1304 fn add_anchor_replaces() {
1305 let mut t = tracker();
1306 let v = t
1307 .register_version("v", 3)
1308 .expect("test: register version v should succeed");
1309 t.add_anchor("dog", v, unit(3, 0))
1310 .expect("test: add anchor dog to version v should succeed");
1311 t.add_anchor("dog", v, unit(3, 1))
1312 .expect("test: replace anchor dog for version v should succeed"); let emb = t
1314 .get_anchor("dog", v)
1315 .expect("test: get replaced anchor dog should succeed");
1316 assert_eq!(emb, &unit(3, 1));
1317 assert_eq!(
1319 t.get_version(v)
1320 .expect("test: get version v should succeed")
1321 .anchor_count,
1322 1
1323 );
1324 }
1325
1326 #[test]
1327 fn add_anchor_dim_mismatch_err() {
1328 let mut t = tracker();
1329 let v = t
1330 .register_version("v", 4)
1331 .expect("test: register version v should succeed");
1332 assert!(t.add_anchor("x", v, vec![1.0, 0.0]).is_err());
1333 }
1334
1335 #[test]
1336 fn add_anchor_empty_concept_err() {
1337 let mut t = tracker();
1338 let v = t
1339 .register_version("v", 2)
1340 .expect("test: register version v should succeed");
1341 assert!(t.add_anchor("", v, vec![1.0, 0.0]).is_err());
1342 }
1343
1344 #[test]
1345 fn add_anchor_unknown_version_err() {
1346 let mut t = tracker();
1347 assert!(t.add_anchor("x", 999, vec![1.0]).is_err());
1348 }
1349
1350 #[test]
1351 fn get_anchor_missing_concept_err() {
1352 let mut t = tracker();
1353 let v = t
1354 .register_version("v", 2)
1355 .expect("test: register version v should succeed");
1356 assert!(t.get_anchor("missing", v).is_err());
1357 }
1358
1359 #[test]
1360 fn get_anchor_missing_version_for_concept_err() {
1361 let mut t = tracker();
1362 let v1 = t
1363 .register_version("v1", 2)
1364 .expect("test: register version v1 should succeed");
1365 let v2 = t
1366 .register_version("v2", 2)
1367 .expect("test: register version v2 should succeed");
1368 t.add_anchor("x", v1, vec![1.0, 0.0])
1369 .expect("test: add anchor x to v1 should succeed");
1370 assert!(t.get_anchor("x", v2).is_err());
1371 }
1372
1373 #[test]
1374 fn anchor_count_increments() {
1375 let mut t = tracker();
1376 let v = t
1377 .register_version("v", 2)
1378 .expect("test: register version v should succeed");
1379 assert_eq!(
1380 t.get_version(v)
1381 .expect("test: get version v should succeed")
1382 .anchor_count,
1383 0
1384 );
1385 t.add_anchor("a", v, vec![1.0, 0.0])
1386 .expect("test: add anchor a to version v should succeed");
1387 assert_eq!(
1388 t.get_version(v)
1389 .expect("test: get version v should succeed after anchor a")
1390 .anchor_count,
1391 1
1392 );
1393 t.add_anchor("b", v, vec![0.0, 1.0])
1394 .expect("test: add anchor b to version v should succeed");
1395 assert_eq!(
1396 t.get_version(v)
1397 .expect("test: get version v should succeed after anchor b")
1398 .anchor_count,
1399 2
1400 );
1401 }
1402
1403 #[test]
1404 fn concepts_for_version_correct() {
1405 let mut t = tracker();
1406 let v1 = t
1407 .register_version("v1", 2)
1408 .expect("test: register version v1 should succeed");
1409 let v2 = t
1410 .register_version("v2", 2)
1411 .expect("test: register version v2 should succeed");
1412 t.add_anchor("cat", v1, vec![1.0, 0.0])
1413 .expect("test: add anchor cat to v1 should succeed");
1414 t.add_anchor("dog", v1, vec![0.0, 1.0])
1415 .expect("test: add anchor dog to v1 should succeed");
1416 t.add_anchor("cat", v2, vec![1.0, 0.0])
1417 .expect("test: add anchor cat to v2 should succeed");
1418 let v1_concepts = t.concepts_for_version(v1);
1419 assert!(v1_concepts.contains(&"cat"));
1420 assert!(v1_concepts.contains(&"dog"));
1421 let v2_concepts = t.concepts_for_version(v2);
1422 assert!(v2_concepts.contains(&"cat"));
1423 assert!(!v2_concepts.contains(&"dog"));
1424 }
1425
1426 #[test]
1429 fn compute_drift_zero_for_identical_embeddings() {
1430 let mut t = tracker_with(0.1);
1431 let v1 = t
1432 .register_version("v1", 3)
1433 .expect("test: register version v1 should succeed");
1434 let v2 = t
1435 .register_version("v2", 3)
1436 .expect("test: register version v2 should succeed");
1437 t.add_anchor("x", v1, vec![1.0, 0.0, 0.0])
1438 .expect("test: add anchor x to v1 should succeed");
1439 t.add_anchor("x", v2, vec![1.0, 0.0, 0.0])
1440 .expect("test: add anchor x to v2 should succeed");
1441 let report = t
1442 .compute_drift(v1, v2)
1443 .expect("test: compute drift with identical embeddings should succeed");
1444 assert!(report.overall_drift < 1e-10);
1445 assert!(report.drifted_concepts.is_empty());
1446 assert!(!report.stable_concepts.is_empty());
1447 }
1448
1449 #[test]
1450 fn compute_drift_max_for_orthogonal() {
1451 let mut t = tracker_with(0.5);
1452 let v1 = t
1453 .register_version("v1", 2)
1454 .expect("test: register version v1 should succeed");
1455 let v2 = t
1456 .register_version("v2", 2)
1457 .expect("test: register version v2 should succeed");
1458 t.add_anchor("x", v1, unit(2, 0))
1459 .expect("test: add anchor x to v1 should succeed");
1460 t.add_anchor("x", v2, unit(2, 1))
1461 .expect("test: add anchor x to v2 should succeed");
1462 let report = t
1463 .compute_drift(v1, v2)
1464 .expect("test: compute drift for orthogonal embeddings should succeed");
1465 assert!((report.overall_drift - 1.0).abs() < 1e-10);
1466 assert_eq!(report.drifted_concepts.len(), 1);
1467 }
1468
1469 #[test]
1470 fn compute_drift_unknown_version_err() {
1471 let mut t = tracker();
1472 let v1 = t
1473 .register_version("v1", 2)
1474 .expect("test: register version v1 should succeed");
1475 assert!(t.compute_drift(v1, 9999).is_err());
1476 assert!(t.compute_drift(9999, v1).is_err());
1477 }
1478
1479 #[test]
1480 fn compute_drift_insufficient_anchors_err() {
1481 let mut t = SemanticVersioningTracker::new(SvtTrackerConfig {
1482 drift_threshold: 0.1,
1483 min_anchors: 3,
1484 window_size: 10,
1485 auto_deprecate: false,
1486 });
1487 let v1 = t
1488 .register_version("v1", 2)
1489 .expect("test: register version v1 should succeed");
1490 let v2 = t
1491 .register_version("v2", 2)
1492 .expect("test: register version v2 should succeed");
1493 t.add_anchor("x", v1, vec![1.0, 0.0])
1494 .expect("test: add anchor x to v1 should succeed");
1495 t.add_anchor("x", v2, vec![1.0, 0.0])
1496 .expect("test: add anchor x to v2 should succeed");
1497 assert!(t.compute_drift(v1, v2).is_err());
1499 }
1500
1501 #[test]
1502 fn compute_drift_records_to_log() {
1503 let mut t = tracker_with(0.1);
1504 let v1 = t
1505 .register_version("v1", 2)
1506 .expect("test: register version v1 should succeed");
1507 let v2 = t
1508 .register_version("v2", 2)
1509 .expect("test: register version v2 should succeed");
1510 t.add_anchor("a", v1, unit(2, 0))
1511 .expect("test: add anchor a to v1 should succeed");
1512 t.add_anchor("a", v2, unit(2, 0))
1513 .expect("test: add anchor a to v2 should succeed");
1514 t.add_anchor("b", v1, unit(2, 1))
1515 .expect("test: add anchor b to v1 should succeed");
1516 t.add_anchor("b", v2, unit(2, 0))
1517 .expect("test: add anchor b to v2 should succeed");
1518 let _ = t
1519 .compute_drift(v1, v2)
1520 .expect("test: compute drift should succeed");
1521 assert_eq!(t.drift_log().len(), 2);
1522 }
1523
1524 #[test]
1525 fn compute_drift_report_has_recommendation() {
1526 let mut t = tracker_with(0.1);
1527 let v1 = t
1528 .register_version("v1", 2)
1529 .expect("test: register version v1 should succeed");
1530 let v2 = t
1531 .register_version("v2", 2)
1532 .expect("test: register version v2 should succeed");
1533 t.add_anchor("x", v1, unit(2, 0))
1534 .expect("test: add anchor x to v1 should succeed");
1535 t.add_anchor("x", v2, unit(2, 0))
1536 .expect("test: add anchor x to v2 should succeed");
1537 let report = t
1538 .compute_drift(v1, v2)
1539 .expect("test: compute drift should succeed");
1540 assert!(!report.recommendation.is_empty());
1541 }
1542
1543 #[test]
1544 fn compute_drift_drifted_sorted_descending() {
1545 let mut t = tracker_with(0.01);
1546 let v1 = t
1547 .register_version("v1", 4)
1548 .expect("test: register version v1 should succeed");
1549 let v2 = t
1550 .register_version("v2", 4)
1551 .expect("test: register version v2 should succeed");
1552 t.add_anchor("a", v1, unit(4, 0))
1554 .expect("test: add anchor a to v1 should succeed");
1555 t.add_anchor("a", v2, unit(4, 1))
1556 .expect("test: add anchor a to v2 should succeed");
1557 t.add_anchor("b", v1, near_unit(4, 0, 0.5))
1560 .expect("test: add anchor b to v1 should succeed");
1561 t.add_anchor("b", v2, unit(4, 0))
1562 .expect("test: add anchor b to v2 should succeed");
1563 let report = t
1564 .compute_drift(v1, v2)
1565 .expect("test: compute drift should succeed");
1566 assert!(report.drifted_concepts.len() >= 2);
1568 for w in report.drifted_concepts.windows(2) {
1570 assert!(w[0].1 >= w[1].1);
1571 }
1572 }
1573
1574 #[test]
1577 fn find_drifted_concepts_above_threshold() {
1578 let mut t = tracker_with(0.3);
1579 let v1 = t
1580 .register_version("v1", 2)
1581 .expect("test: register version v1 should succeed");
1582 let v2 = t
1583 .register_version("v2", 2)
1584 .expect("test: register version v2 should succeed");
1585 t.add_anchor("same", v1, unit(2, 0))
1586 .expect("test: add anchor same to v1 should succeed");
1587 t.add_anchor("same", v2, unit(2, 0))
1588 .expect("test: add anchor same to v2 should succeed");
1589 t.add_anchor("diff", v1, unit(2, 0))
1590 .expect("test: add anchor diff to v1 should succeed");
1591 t.add_anchor("diff", v2, unit(2, 1))
1592 .expect("test: add anchor diff to v2 should succeed");
1593 let drifted = t
1594 .find_drifted_concepts(v1, v2, 0.3)
1595 .expect("test: find drifted concepts should succeed");
1596 assert_eq!(drifted.len(), 1);
1597 assert_eq!(drifted[0].0, "diff");
1598 }
1599
1600 #[test]
1601 fn find_drifted_concepts_unknown_version_err() {
1602 let mut t = tracker();
1603 let v = t
1604 .register_version("v", 2)
1605 .expect("test: register version v should succeed");
1606 assert!(t.find_drifted_concepts(v, 999, 0.1).is_err());
1607 }
1608
1609 #[test]
1612 fn semantic_similarity_over_time_basic() {
1613 let mut t = tracker_with(0.1);
1614 let v1 = t
1615 .register_version("v1", 3)
1616 .expect("test: register version v1 should succeed");
1617 let v2 = t
1618 .register_version("v2", 3)
1619 .expect("test: register version v2 should succeed");
1620 let v3 = t
1621 .register_version("v3", 3)
1622 .expect("test: register version v3 should succeed");
1623 t.add_anchor("cat", v1, unit(3, 0))
1624 .expect("test: add anchor cat to v1 should succeed");
1625 t.add_anchor("cat", v2, near_unit(3, 0, 0.01))
1626 .expect("test: add anchor cat to v2 should succeed");
1627 t.add_anchor("cat", v3, unit(3, 0))
1628 .expect("test: add anchor cat to v3 should succeed");
1629 let pairs = t
1630 .semantic_similarity_over_time("cat")
1631 .expect("test: semantic similarity over time for cat should succeed");
1632 assert_eq!(pairs.len(), 2);
1633 for (_, _, sim) in &pairs {
1635 assert!(*sim > 0.9);
1636 }
1637 }
1638
1639 #[test]
1640 fn semantic_similarity_over_time_empty_concept_err() {
1641 let t = tracker();
1642 assert!(t.semantic_similarity_over_time("").is_err());
1643 }
1644
1645 #[test]
1646 fn semantic_similarity_over_time_missing_concept_err() {
1647 let t = tracker();
1648 assert!(t.semantic_similarity_over_time("ghost").is_err());
1649 }
1650
1651 #[test]
1652 fn semantic_similarity_over_time_single_version_err() {
1653 let mut t = tracker();
1654 let v = t
1655 .register_version("v", 2)
1656 .expect("test: register version v should succeed");
1657 t.add_anchor("x", v, unit(2, 0))
1658 .expect("test: add anchor x to v should succeed");
1659 assert!(t.semantic_similarity_over_time("x").is_err());
1660 }
1661
1662 #[test]
1663 fn semantic_similarity_over_time_window_limit() {
1664 let mut t = SemanticVersioningTracker::new(SvtTrackerConfig {
1665 drift_threshold: 0.1,
1666 min_anchors: 1,
1667 window_size: 3,
1668 auto_deprecate: false,
1669 });
1670 for i in 0..10usize {
1671 let v = t
1672 .register_version(format!("v{i}"), 2)
1673 .expect("test: register version should succeed");
1674 t.add_anchor("x", v, vec![1.0, i as f64])
1675 .expect("test: add anchor x to version should succeed");
1676 }
1677 let pairs = t
1678 .semantic_similarity_over_time("x")
1679 .expect("test: semantic similarity over time for x should succeed");
1680 assert!(pairs.len() <= 3);
1681 }
1682
1683 #[test]
1686 fn stability_score_no_anchors_returns_one() {
1687 let t = tracker();
1688 let s = t
1690 .stability_score("ghost")
1691 .expect("test: stability score for unregistered concept should return Ok(1.0)");
1692 assert!((s - 1.0).abs() < 1e-10);
1693 }
1694
1695 #[test]
1696 fn stability_score_one_version_returns_one() {
1697 let mut t = tracker();
1698 let v = t
1699 .register_version("v", 2)
1700 .expect("test: register version v should succeed");
1701 t.add_anchor("x", v, unit(2, 0))
1702 .expect("test: add anchor x to v should succeed");
1703 let s = t
1704 .stability_score("x")
1705 .expect("test: stability score should succeed");
1706 assert!((s - 1.0).abs() < 1e-10);
1707 }
1708
1709 #[test]
1710 fn stability_score_identical_embeddings_is_one() {
1711 let mut t = tracker();
1712 let v1 = t
1713 .register_version("v1", 2)
1714 .expect("test: register version v1 should succeed");
1715 let v2 = t
1716 .register_version("v2", 2)
1717 .expect("test: register version v2 should succeed");
1718 t.add_anchor("x", v1, unit(2, 0))
1719 .expect("test: add anchor x to v1 should succeed");
1720 t.add_anchor("x", v2, unit(2, 0))
1721 .expect("test: add anchor x to v2 should succeed");
1722 let s = t
1723 .stability_score("x")
1724 .expect("test: stability score for identical embeddings should succeed");
1725 assert!((s - 1.0).abs() < 1e-10);
1726 }
1727
1728 #[test]
1729 fn stability_score_orthogonal_embeddings_is_zero() {
1730 let mut t = tracker();
1731 let v1 = t
1732 .register_version("v1", 2)
1733 .expect("test: register version v1 should succeed");
1734 let v2 = t
1735 .register_version("v2", 2)
1736 .expect("test: register version v2 should succeed");
1737 t.add_anchor("x", v1, unit(2, 0))
1738 .expect("test: add anchor x to v1 should succeed");
1739 t.add_anchor("x", v2, unit(2, 1))
1740 .expect("test: add anchor x to v2 should succeed");
1741 let s = t
1742 .stability_score("x")
1743 .expect("test: stability score for orthogonal embeddings should succeed");
1744 assert!(s.abs() < 1e-10);
1745 }
1746
1747 #[test]
1748 fn stability_score_empty_concept_err() {
1749 let t = tracker();
1750 assert!(t.stability_score("").is_err());
1751 }
1752
1753 #[test]
1756 fn recommend_migration_returns_drifted() {
1757 let mut t = tracker_with(0.3);
1758 let v1 = t
1759 .register_version("v1", 2)
1760 .expect("test: register version v1 should succeed");
1761 let v2 = t
1762 .register_version("v2", 2)
1763 .expect("test: register version v2 should succeed");
1764 t.add_anchor("stable", v1, unit(2, 0))
1765 .expect("test: add anchor stable to v1 should succeed");
1766 t.add_anchor("stable", v2, unit(2, 0))
1767 .expect("test: add anchor stable to v2 should succeed");
1768 t.add_anchor("drifted", v1, unit(2, 0))
1769 .expect("test: add anchor drifted to v1 should succeed");
1770 t.add_anchor("drifted", v2, unit(2, 1))
1771 .expect("test: add anchor drifted to v2 should succeed");
1772 let recs = t
1773 .recommend_migration(v1, v2)
1774 .expect("test: recommend migration should succeed");
1775 assert!(recs.contains(&"drifted".to_string()));
1776 assert!(!recs.contains(&"stable".to_string()));
1777 }
1778
1779 #[test]
1780 fn recommend_migration_unknown_version_err() {
1781 let mut t = tracker();
1782 let v = t
1783 .register_version("v", 2)
1784 .expect("test: register version v should succeed");
1785 assert!(t.recommend_migration(v, 9999).is_err());
1786 }
1787
1788 #[test]
1791 fn tracker_stats_empty() {
1792 let t = tracker();
1793 let stats = t.tracker_stats();
1794 assert_eq!(stats.total_versions, 0);
1795 assert_eq!(stats.active_versions, 0);
1796 assert_eq!(stats.total_anchors, 0);
1797 assert_eq!(stats.distinct_concepts, 0);
1798 assert_eq!(stats.drift_events, 0);
1799 assert!((stats.mean_logged_drift).abs() < 1e-10);
1800 }
1801
1802 #[test]
1803 fn tracker_stats_after_operations() {
1804 let mut t = tracker_with(0.1);
1805 let v1 = t
1806 .register_version("v1", 2)
1807 .expect("test: register version v1 should succeed");
1808 let v2 = t
1809 .register_version("v2", 2)
1810 .expect("test: register version v2 should succeed");
1811 t.deprecate_version(v1)
1812 .expect("test: deprecate version v1 should succeed");
1813 t.add_anchor("x", v1, unit(2, 0))
1814 .expect("test: add anchor x to v1 should succeed");
1815 t.add_anchor("x", v2, unit(2, 0))
1816 .expect("test: add anchor x to v2 should succeed");
1817 t.add_anchor("y", v1, unit(2, 1))
1818 .expect("test: add anchor y to v1 should succeed");
1819 t.add_anchor("y", v2, unit(2, 1))
1820 .expect("test: add anchor y to v2 should succeed");
1821 let _ = t
1822 .compute_drift(v1, v2)
1823 .expect("test: compute drift should succeed");
1824 let stats = t.tracker_stats();
1825 assert_eq!(stats.total_versions, 2);
1826 assert_eq!(stats.active_versions, 1);
1827 assert_eq!(stats.distinct_concepts, 2);
1828 assert_eq!(stats.total_anchors, 4);
1829 assert_eq!(stats.drift_events, 2);
1830 }
1831
1832 #[test]
1835 fn drift_log_bounded_at_500() {
1836 let mut t = tracker_with(0.0);
1837 let v1 = t
1838 .register_version("v1", 2)
1839 .expect("test: register version v1 should succeed");
1840 let v2 = t
1841 .register_version("v2", 2)
1842 .expect("test: register version v2 should succeed");
1843 for i in 0usize..600 {
1845 let c = format!("c{i}");
1846 t.add_anchor(&c, v1, unit(2, 0))
1847 .expect("test: add anchor to v1 should succeed");
1848 t.add_anchor(&c, v2, unit(2, 1))
1849 .expect("test: add anchor to v2 should succeed");
1850 }
1851 let _ = t
1852 .compute_drift(v1, v2)
1853 .expect("test: compute drift with 600 concepts should succeed");
1854 assert!(t.drift_log().len() <= 500);
1855 }
1856
1857 #[test]
1858 fn clear_drift_log() {
1859 let mut t = tracker_with(0.1);
1860 let v1 = t
1861 .register_version("v1", 2)
1862 .expect("test: register version v1 should succeed");
1863 let v2 = t
1864 .register_version("v2", 2)
1865 .expect("test: register version v2 should succeed");
1866 t.add_anchor("x", v1, unit(2, 0))
1867 .expect("test: add anchor x to v1 should succeed");
1868 t.add_anchor("x", v2, unit(2, 0))
1869 .expect("test: add anchor x to v2 should succeed");
1870 let _ = t
1871 .compute_drift(v1, v2)
1872 .expect("test: compute drift should succeed");
1873 assert!(!t.drift_log().is_empty());
1874 t.clear_drift_log();
1875 assert!(t.drift_log().is_empty());
1876 }
1877
1878 #[test]
1881 fn add_anchors_batch_partial_failure() {
1882 let mut t = tracker();
1883 let v = t
1884 .register_version("v", 2)
1885 .expect("test: register version v should succeed");
1886 let items = vec![
1887 ("good".to_string(), v, vec![1.0, 0.0]),
1888 ("bad".to_string(), v, vec![1.0, 0.0, 0.0]), ];
1890 let errors = t.add_anchors_batch(items);
1891 assert_eq!(errors.len(), 1);
1892 assert!(t.get_anchor("good", v).is_ok());
1894 }
1895
1896 #[test]
1897 fn compute_all_consecutive_drifts() {
1898 let mut t = tracker_with(0.1);
1899 let v1 = t
1900 .register_version("v1", 2)
1901 .expect("test: register version v1 should succeed");
1902 let v2 = t
1903 .register_version("v2", 2)
1904 .expect("test: register version v2 should succeed");
1905 let v3 = t
1906 .register_version("v3", 2)
1907 .expect("test: register version v3 should succeed");
1908 for v in [v1, v2, v3] {
1909 t.add_anchor("x", v, unit(2, 0))
1910 .expect("test: add anchor x to version should succeed");
1911 }
1912 let results = t.compute_all_consecutive_drifts();
1913 assert_eq!(results.len(), 2);
1914 for r in results {
1915 assert!(r.is_ok());
1916 }
1917 }
1918
1919 #[test]
1922 fn global_stability_no_concepts_is_one() {
1923 let t = tracker();
1924 assert!((t.global_stability() - 1.0).abs() < 1e-10);
1925 }
1926
1927 #[test]
1928 fn global_stability_with_perfect_concepts() {
1929 let mut t = tracker();
1930 let v1 = t
1931 .register_version("v1", 2)
1932 .expect("test: register version v1 should succeed");
1933 let v2 = t
1934 .register_version("v2", 2)
1935 .expect("test: register version v2 should succeed");
1936 t.add_anchor("x", v1, unit(2, 0))
1937 .expect("test: add anchor x to v1 should succeed");
1938 t.add_anchor("x", v2, unit(2, 0))
1939 .expect("test: add anchor x to v2 should succeed");
1940 let gs = t.global_stability();
1941 assert!((gs - 1.0).abs() < 1e-10);
1942 }
1943
1944 #[test]
1947 fn concepts_by_stability_sorted() {
1948 let mut t = tracker();
1949 let v1 = t
1950 .register_version("v1", 2)
1951 .expect("test: register version v1 should succeed");
1952 let v2 = t
1953 .register_version("v2", 2)
1954 .expect("test: register version v2 should succeed");
1955 t.add_anchor("stable", v1, unit(2, 0))
1956 .expect("test: add anchor stable to v1 should succeed");
1957 t.add_anchor("stable", v2, unit(2, 0))
1958 .expect("test: add anchor stable to v2 should succeed");
1959 t.add_anchor("unstable", v1, unit(2, 0))
1960 .expect("test: add anchor unstable to v1 should succeed");
1961 t.add_anchor("unstable", v2, unit(2, 1))
1962 .expect("test: add anchor unstable to v2 should succeed");
1963 let sorted = t.concepts_by_stability();
1964 assert_eq!(sorted[0].0, "stable");
1966 assert_eq!(sorted[1].0, "unstable");
1967 }
1968
1969 #[test]
1972 fn top_drifted_concepts_limits_result() {
1973 let mut t = tracker_with(0.0);
1974 let v1 = t
1975 .register_version("v1", 4)
1976 .expect("test: register version v1 should succeed");
1977 let v2 = t
1978 .register_version("v2", 4)
1979 .expect("test: register version v2 should succeed");
1980 for i in 0..4usize {
1981 let c = format!("c{i}");
1982 t.add_anchor(&c, v1, unit(4, 0))
1983 .expect("test: add anchor to v1 should succeed");
1984 t.add_anchor(&c, v2, unit(4, (i + 1) % 4))
1985 .expect("test: add anchor to v2 should succeed");
1986 }
1987 let top2 = t
1988 .top_drifted_concepts(v1, v2, 2)
1989 .expect("test: top drifted concepts should succeed");
1990 assert!(top2.len() <= 2);
1991 }
1992
1993 #[test]
1996 fn are_compatible_identical_true() {
1997 let mut t = tracker_with(0.1);
1998 let v1 = t
1999 .register_version("v1", 2)
2000 .expect("test: register version v1 should succeed");
2001 let v2 = t
2002 .register_version("v2", 2)
2003 .expect("test: register version v2 should succeed");
2004 t.add_anchor("x", v1, unit(2, 0))
2005 .expect("test: add anchor x to v1 should succeed");
2006 t.add_anchor("x", v2, unit(2, 0))
2007 .expect("test: add anchor x to v2 should succeed");
2008 assert!(t
2009 .are_compatible(v1, v2)
2010 .expect("test: are_compatible should succeed"));
2011 }
2012
2013 #[test]
2014 fn are_compatible_orthogonal_false() {
2015 let mut t = tracker_with(0.1);
2016 let v1 = t
2017 .register_version("v1", 2)
2018 .expect("test: register version v1 should succeed");
2019 let v2 = t
2020 .register_version("v2", 2)
2021 .expect("test: register version v2 should succeed");
2022 t.add_anchor("x", v1, unit(2, 0))
2023 .expect("test: add anchor x to v1 should succeed");
2024 t.add_anchor("x", v2, unit(2, 1))
2025 .expect("test: add anchor x to v2 should succeed");
2026 assert!(!t
2027 .are_compatible(v1, v2)
2028 .expect("test: are_compatible should succeed"));
2029 }
2030
2031 #[test]
2034 fn remove_version_anchors_clears_entries() {
2035 let mut t = tracker();
2036 let v1 = t
2037 .register_version("v1", 2)
2038 .expect("test: register version v1 should succeed");
2039 let v2 = t
2040 .register_version("v2", 2)
2041 .expect("test: register version v2 should succeed");
2042 t.add_anchor("x", v1, unit(2, 0))
2043 .expect("test: add anchor x to v1 should succeed");
2044 t.add_anchor("x", v2, unit(2, 0))
2045 .expect("test: add anchor x to v2 should succeed");
2046 let removed = t
2047 .remove_version_anchors(v1)
2048 .expect("test: remove version anchors should succeed");
2049 assert_eq!(removed, 1);
2050 assert!(t.get_anchor("x", v1).is_err());
2051 assert!(t.get_anchor("x", v2).is_ok());
2052 }
2053
2054 #[test]
2055 fn remove_version_anchors_unknown_version_err() {
2056 let mut t = tracker();
2057 assert!(t.remove_version_anchors(9999).is_err());
2058 }
2059
2060 #[test]
2063 fn concept_drift_matrix_correct() {
2064 let mut t = tracker();
2065 let v1 = t
2066 .register_version("v1", 2)
2067 .expect("test: register version v1 should succeed");
2068 let v2 = t
2069 .register_version("v2", 2)
2070 .expect("test: register version v2 should succeed");
2071 t.add_anchor("same", v1, unit(2, 0))
2072 .expect("test: add anchor same to v1 should succeed");
2073 t.add_anchor("same", v2, unit(2, 0))
2074 .expect("test: add anchor same to v2 should succeed");
2075 t.add_anchor("diff", v1, unit(2, 0))
2076 .expect("test: add anchor diff to v1 should succeed");
2077 t.add_anchor("diff", v2, unit(2, 1))
2078 .expect("test: add anchor diff to v2 should succeed");
2079 let matrix = t
2080 .concept_drift_matrix(v1, v2)
2081 .expect("test: concept drift matrix should succeed");
2082 assert!((matrix["same"]).abs() < 1e-10);
2083 assert!((matrix["diff"] - 1.0).abs() < 1e-10);
2084 }
2085
2086 #[test]
2087 fn concept_drift_matrix_does_not_log() {
2088 let mut t = tracker();
2089 let v1 = t
2090 .register_version("v1", 2)
2091 .expect("test: register version v1 should succeed");
2092 let v2 = t
2093 .register_version("v2", 2)
2094 .expect("test: register version v2 should succeed");
2095 t.add_anchor("x", v1, unit(2, 0))
2096 .expect("test: add anchor x to v1 should succeed");
2097 t.add_anchor("x", v2, unit(2, 0))
2098 .expect("test: add anchor x to v2 should succeed");
2099 let _ = t
2100 .concept_drift_matrix(v1, v2)
2101 .expect("test: concept drift matrix should succeed");
2102 assert!(t.drift_log().is_empty());
2104 }
2105
2106 #[test]
2109 fn auto_deprecate_deactivates_old_version() {
2110 let mut t = SemanticVersioningTracker::new(SvtTrackerConfig {
2111 drift_threshold: 0.1,
2112 min_anchors: 1,
2113 window_size: 10,
2114 auto_deprecate: true,
2115 });
2116 let v1 = t
2117 .register_version("v1", 2)
2118 .expect("test: register version v1 should succeed");
2119 let v2 = t
2120 .register_version("v2", 2)
2121 .expect("test: register version v2 should succeed");
2122 t.add_anchor("x", v1, unit(2, 0))
2124 .expect("test: add anchor x to v1 should succeed");
2125 t.add_anchor("x", v2, unit(2, 1))
2126 .expect("test: add anchor x to v2 should succeed");
2127 let _ = t
2128 .compute_drift(v1, v2)
2129 .expect("test: compute drift should succeed");
2130 assert!(
2131 !t.get_version(v1)
2132 .expect("test: get version v1 after auto-deprecate should succeed")
2133 .is_active
2134 );
2135 }
2136
2137 #[test]
2138 fn auto_deprecate_does_not_deactivate_when_drift_low() {
2139 let mut t = SemanticVersioningTracker::new(SvtTrackerConfig {
2140 drift_threshold: 0.5,
2141 min_anchors: 1,
2142 window_size: 10,
2143 auto_deprecate: true,
2144 });
2145 let v1 = t
2146 .register_version("v1", 2)
2147 .expect("test: register version v1 should succeed");
2148 let v2 = t
2149 .register_version("v2", 2)
2150 .expect("test: register version v2 should succeed");
2151 t.add_anchor("x", v1, unit(2, 0))
2152 .expect("test: add anchor x to v1 should succeed");
2153 t.add_anchor("x", v2, unit(2, 0))
2154 .expect("test: add anchor x to v2 should succeed");
2155 let _ = t
2156 .compute_drift(v1, v2)
2157 .expect("test: compute drift should succeed");
2158 assert!(
2160 t.get_version(v1)
2161 .expect("test: get version v1 should succeed")
2162 .is_active
2163 );
2164 }
2165
2166 #[test]
2169 fn recommendation_transparent_when_very_low_drift() {
2170 let mut t = tracker_with(0.5);
2171 let v1 = t
2172 .register_version("v1", 2)
2173 .expect("test: register version v1 should succeed");
2174 let v2 = t
2175 .register_version("v2", 2)
2176 .expect("test: register version v2 should succeed");
2177 t.add_anchor("x", v1, unit(2, 0))
2178 .expect("test: add anchor x to v1 should succeed");
2179 t.add_anchor("x", v2, unit(2, 0))
2180 .expect("test: add anchor x to v2 should succeed");
2181 let r = t
2182 .compute_drift(v1, v2)
2183 .expect("test: compute drift should succeed");
2184 assert!(
2185 r.recommendation.contains("transparent") || r.recommendation.contains("compatible")
2186 );
2187 }
2188
2189 #[test]
2190 fn recommendation_full_reindex_when_very_high_drift() {
2191 let mut t = tracker_with(0.1);
2192 let v1 = t
2193 .register_version("v1", 2)
2194 .expect("test: register version v1 should succeed");
2195 let v2 = t
2196 .register_version("v2", 2)
2197 .expect("test: register version v2 should succeed");
2198 t.add_anchor("x", v1, unit(2, 0))
2199 .expect("test: add anchor x to v1 should succeed");
2200 t.add_anchor("x", v2, unit(2, 1))
2201 .expect("test: add anchor x to v2 should succeed");
2202 let r = t
2203 .compute_drift(v1, v2)
2204 .expect("test: compute drift should succeed");
2205 assert!(r.recommendation.contains("incompatible") || r.recommendation.contains("re-index"));
2206 }
2207
2208 #[test]
2211 fn drift_event_significant_flag() {
2212 let mut t = tracker_with(0.3);
2213 let v1 = t
2214 .register_version("v1", 2)
2215 .expect("test: register version v1 should succeed");
2216 let v2 = t
2217 .register_version("v2", 2)
2218 .expect("test: register version v2 should succeed");
2219 t.add_anchor("x", v1, unit(2, 0))
2220 .expect("test: add anchor x to v1 should succeed");
2221 t.add_anchor("x", v2, unit(2, 1))
2222 .expect("test: add anchor x to v2 should succeed"); let _ = t
2224 .compute_drift(v1, v2)
2225 .expect("test: compute drift should succeed");
2226 let event = t
2227 .drift_log()
2228 .back()
2229 .expect("test: drift log should have at least one event after compute_drift");
2230 assert!(event.is_significant);
2231 assert!((event.drift_score - 1.0).abs() < 1e-10);
2232 }
2233
2234 #[test]
2235 fn drift_event_not_significant_for_identical() {
2236 let mut t = tracker_with(0.3);
2237 let v1 = t
2238 .register_version("v1", 2)
2239 .expect("test: register version v1 should succeed");
2240 let v2 = t
2241 .register_version("v2", 2)
2242 .expect("test: register version v2 should succeed");
2243 t.add_anchor("x", v1, unit(2, 0))
2244 .expect("test: add anchor x to v1 should succeed");
2245 t.add_anchor("x", v2, unit(2, 0))
2246 .expect("test: add anchor x to v2 should succeed");
2247 let _ = t
2248 .compute_drift(v1, v2)
2249 .expect("test: compute drift should succeed");
2250 let event = t
2251 .drift_log()
2252 .back()
2253 .expect("test: drift log should have at least one event after compute_drift");
2254 assert!(!event.is_significant);
2255 }
2256
2257 #[test]
2260 fn config_accessor_returns_correct_threshold() {
2261 let t = tracker_with(0.42);
2262 assert!((t.config().drift_threshold - 0.42).abs() < 1e-10);
2263 }
2264
2265 #[test]
2266 fn config_mut_modifies_threshold() {
2267 let mut t = tracker_with(0.1);
2268 t.config_mut().drift_threshold = 0.9;
2269 assert!((t.config().drift_threshold - 0.9).abs() < 1e-10);
2270 }
2271}