Skip to main content

ipfrs_semantic/
semantic_versioning.rs

1//! Semantic document/embedding versioning with change detection,
2//! compatibility analysis, and migration paths.
3//!
4//! Provides [`SemanticVersioningEngine`] for managing versioned artifacts with
5//! full SemVer 2.0 parsing, compatibility matrices, and changelog tracking.
6
7use std::collections::HashMap;
8use std::fmt;
9
10// ---------------------------------------------------------------------------
11// Errors
12// ---------------------------------------------------------------------------
13
14/// Errors produced by the semantic versioning subsystem.
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum SemVerError {
17    /// A version string could not be parsed.
18    ParseError(String),
19    /// No artifact with the given ID was found.
20    ArtifactNotFound(String),
21    /// An artifact with the given ID already exists.
22    ArtifactAlreadyExists(String),
23    /// A version value is logically invalid.
24    InvalidVersion(String),
25}
26
27impl fmt::Display for SemVerError {
28    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29        match self {
30            SemVerError::ParseError(s) => write!(f, "SemVer parse error: {s}"),
31            SemVerError::ArtifactNotFound(s) => write!(f, "artifact not found: {s}"),
32            SemVerError::ArtifactAlreadyExists(s) => write!(f, "artifact already exists: {s}"),
33            SemVerError::InvalidVersion(s) => write!(f, "invalid version: {s}"),
34        }
35    }
36}
37
38impl std::error::Error for SemVerError {}
39
40// ---------------------------------------------------------------------------
41// SemVer
42// ---------------------------------------------------------------------------
43
44/// A Semantic Versioning 2.0 version triple plus optional pre-release and
45/// build-metadata labels.
46///
47/// Ordering and equality are based solely on `(major, minor, patch)`.
48#[derive(Debug, Clone, Eq)]
49pub struct SemVer {
50    /// Major version — breaking changes.
51    pub major: u32,
52    /// Minor version — backward-compatible new features.
53    pub minor: u32,
54    /// Patch version — backward-compatible bug fixes.
55    pub patch: u32,
56    /// Optional pre-release identifier (e.g. `"alpha"`, `"beta.1"`).
57    pub pre_release: Option<String>,
58    /// Optional build metadata (e.g. `"build.1"`, `"20240101"`).
59    pub build_metadata: Option<String>,
60}
61
62impl SemVer {
63    /// Construct a new version without pre-release or build metadata.
64    pub fn new(major: u32, minor: u32, patch: u32) -> Self {
65        Self {
66            major,
67            minor,
68            patch,
69            pre_release: None,
70            build_metadata: None,
71        }
72    }
73
74    /// Parse a SemVer string such as `"1.2.3"`, `"1.2.3-alpha"`,
75    /// `"1.2.3+build.1"`, or `"1.2.3-alpha+build"`.
76    pub fn parse(s: &str) -> Result<SemVer, SemVerError> {
77        // Split off build metadata first (after '+')
78        let (version_and_pre, build_metadata) = match s.split_once('+') {
79            Some((left, right)) => (left, Some(right.to_string())),
80            None => (s, None),
81        };
82
83        // Split off pre-release (after '-')
84        let (version_part, pre_release) = match version_and_pre.split_once('-') {
85            Some((left, right)) => (left, Some(right.to_string())),
86            None => (version_and_pre, None),
87        };
88
89        // Parse X.Y.Z
90        let parts: Vec<&str> = version_part.split('.').collect();
91        if parts.len() != 3 {
92            return Err(SemVerError::ParseError(format!(
93                "expected X.Y.Z, got {version_part:?}"
94            )));
95        }
96
97        let parse_u32 = |raw: &str| -> Result<u32, SemVerError> {
98            raw.parse::<u32>().map_err(|_| {
99                SemVerError::ParseError(format!("non-numeric version component: {raw:?}"))
100            })
101        };
102
103        Ok(SemVer {
104            major: parse_u32(parts[0])?,
105            minor: parse_u32(parts[1])?,
106            patch: parse_u32(parts[2])?,
107            pre_release,
108            build_metadata,
109        })
110    }
111
112    /// Increment the major version and zero out minor and patch.
113    /// Clears pre-release and build metadata.
114    pub fn bump_major(&self) -> SemVer {
115        SemVer::new(self.major + 1, 0, 0)
116    }
117
118    /// Increment the minor version and zero out patch.
119    /// Clears pre-release and build metadata.
120    pub fn bump_minor(&self) -> SemVer {
121        SemVer::new(self.major, self.minor + 1, 0)
122    }
123
124    /// Increment the patch version.
125    /// Clears pre-release and build metadata.
126    pub fn bump_patch(&self) -> SemVer {
127        SemVer::new(self.major, self.minor, self.patch + 1)
128    }
129
130    /// Returns `true` when `self` is compatible with `other`:
131    /// same major version AND `self >= other`.
132    pub fn is_compatible_with(&self, other: &SemVer) -> bool {
133        self.major == other.major && self >= other
134    }
135}
136
137impl PartialEq for SemVer {
138    fn eq(&self, other: &Self) -> bool {
139        (self.major, self.minor, self.patch) == (other.major, other.minor, other.patch)
140    }
141}
142
143impl PartialOrd for SemVer {
144    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
145        Some(self.cmp(other))
146    }
147}
148
149impl Ord for SemVer {
150    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
151        (self.major, self.minor, self.patch).cmp(&(other.major, other.minor, other.patch))
152    }
153}
154
155impl fmt::Display for SemVer {
156    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
157        write!(f, "{}.{}.{}", self.major, self.minor, self.patch)?;
158        if let Some(pre) = &self.pre_release {
159            write!(f, "-{pre}")?;
160        }
161        if let Some(build) = &self.build_metadata {
162            write!(f, "+{build}")?;
163        }
164        Ok(())
165    }
166}
167
168impl Default for SemVer {
169    fn default() -> Self {
170        SemVer::new(0, 1, 0)
171    }
172}
173
174// ---------------------------------------------------------------------------
175// ChangeType / BumpType
176// ---------------------------------------------------------------------------
177
178/// The semantic category of a change.
179#[derive(Debug, Clone, Copy, PartialEq, Eq)]
180pub enum ChangeType {
181    /// A breaking, backward-incompatible change.
182    Breaking,
183    /// A new backward-compatible feature.
184    Feature,
185    /// A backward-compatible bug fix.
186    Fix,
187    /// Documentation-only change.
188    Documentation,
189    /// Code restructuring without observable behaviour change.
190    Refactor,
191}
192
193/// The kind of SemVer bump required to publish a [`ChangeType`].
194#[derive(Debug, Clone, Copy, PartialEq, Eq)]
195pub enum BumpType {
196    /// Increment the major component.
197    Major,
198    /// Increment the minor component.
199    Minor,
200    /// Increment the patch component.
201    Patch,
202}
203
204impl ChangeType {
205    /// Map this change category to the minimum required version bump.
206    pub fn required_bump(self) -> BumpType {
207        match self {
208            ChangeType::Breaking => BumpType::Major,
209            ChangeType::Feature => BumpType::Minor,
210            ChangeType::Fix | ChangeType::Documentation | ChangeType::Refactor => BumpType::Patch,
211        }
212    }
213}
214
215// ---------------------------------------------------------------------------
216// ChangeRecord
217// ---------------------------------------------------------------------------
218
219/// A single entry in an artifact's changelog.
220#[derive(Debug, Clone)]
221pub struct ChangeRecord {
222    /// The version after this change was applied.
223    pub version: SemVer,
224    /// The semantic category of the change.
225    pub change_type: ChangeType,
226    /// Human-readable description of the change.
227    pub description: String,
228    /// Unix timestamp (seconds) when the change was recorded.
229    pub timestamp: u64,
230}
231
232// ---------------------------------------------------------------------------
233// VersionedArtifact
234// ---------------------------------------------------------------------------
235
236/// An artifact (e.g. a document or embedding index) tracked by the versioning
237/// engine.
238#[derive(Debug, Clone)]
239pub struct VersionedArtifact {
240    /// Stable identifier for this artifact.
241    pub id: String,
242    /// Current version.
243    pub version: SemVer,
244    /// FNV-1a hash of the artifact's content bytes.
245    pub content_hash: u64,
246    /// Dimensionality of the associated embedding, if any.
247    pub embedding_dim: Option<usize>,
248    /// Unix timestamp (seconds) when the artifact was first registered.
249    pub created_at: u64,
250    /// Full history of changes applied to this artifact.
251    pub changelog: Vec<ChangeRecord>,
252}
253
254impl VersionedArtifact {
255    /// Compute the FNV-1a 64-bit hash of arbitrary bytes.
256    pub fn fnv1a(data: &[u8]) -> u64 {
257        const FNV_OFFSET: u64 = 14_695_981_039_346_656_037;
258        const FNV_PRIME: u64 = 1_099_511_628_211;
259        data.iter().fold(FNV_OFFSET, |acc, &b| {
260            (acc ^ (b as u64)).wrapping_mul(FNV_PRIME)
261        })
262    }
263}
264
265// ---------------------------------------------------------------------------
266// CompatibilityLevel / CompatibilityMatrix
267// ---------------------------------------------------------------------------
268
269/// The degree of compatibility between two major versions.
270#[derive(Debug, Clone, Copy, PartialEq, Eq)]
271pub enum CompatibilityLevel {
272    /// Both directions are fully compatible.
273    FullyCompatible,
274    /// Newer code can read data written by older code.
275    BackwardCompatible,
276    /// Older code can read data written by newer code.
277    ForwardCompatible,
278    /// No compatibility guarantee.
279    Incompatible,
280}
281
282/// Stores pairwise compatibility levels keyed by `(from_major, to_major)`.
283#[derive(Debug, Clone, Default)]
284pub struct CompatibilityMatrix {
285    /// Explicit entries; missing pairs default to [`CompatibilityLevel::Incompatible`].
286    pub entries: HashMap<(u32, u32), CompatibilityLevel>,
287}
288
289impl CompatibilityMatrix {
290    /// Return the compatibility level for the given major version pair,
291    /// defaulting to `Incompatible` when no entry is found.
292    pub fn get(&self, from_major: u32, to_major: u32) -> CompatibilityLevel {
293        *self
294            .entries
295            .get(&(from_major, to_major))
296            .unwrap_or(&CompatibilityLevel::Incompatible)
297    }
298
299    /// Insert a compatibility entry.
300    pub fn set(&mut self, from_major: u32, to_major: u32, level: CompatibilityLevel) {
301        self.entries.insert((from_major, to_major), level);
302    }
303}
304
305// ---------------------------------------------------------------------------
306// VersioningStats
307// ---------------------------------------------------------------------------
308
309/// Aggregate statistics produced by [`SemanticVersioningEngine::stats`].
310#[derive(Debug, Clone)]
311pub struct VersioningStats {
312    /// Number of artifacts currently registered.
313    pub total_artifacts: usize,
314    /// Total change records across all artifacts.
315    pub total_changes: usize,
316    /// Total breaking changes across all artifacts.
317    pub breaking_changes: usize,
318    /// Mean `(major, minor, patch)` formatted as a SemVer string.
319    pub avg_version: String,
320    /// Highest version seen across all artifacts, if any artifacts exist.
321    pub latest_version: Option<SemVer>,
322}
323
324// ---------------------------------------------------------------------------
325// SemanticVersioningEngine
326// ---------------------------------------------------------------------------
327
328/// Manages versioned artifacts with SemVer-based change detection,
329/// compatibility analysis, and migration-path computation.
330pub struct SemanticVersioningEngine {
331    /// Registered artifacts keyed by their ID.
332    pub artifacts: HashMap<String, VersionedArtifact>,
333    /// Rules governing cross-version compatibility.
334    pub compatibility_rules: CompatibilityMatrix,
335}
336
337impl SemanticVersioningEngine {
338    /// Create a new engine with sensible default compatibility rules:
339    ///
340    /// * Same major (distance 0) → [`CompatibilityLevel::FullyCompatible`].
341    /// * Adjacent major (distance 1) → [`CompatibilityLevel::BackwardCompatible`].
342    /// * All other pairs → [`CompatibilityLevel::Incompatible`].
343    ///
344    /// Rules are pre-populated for majors `0..=9`.
345    pub fn new() -> Self {
346        let mut matrix = CompatibilityMatrix::default();
347        // Pre-populate for major versions 0..=9
348        for m in 0u32..=9 {
349            // Same major: fully compatible
350            matrix.set(m, m, CompatibilityLevel::FullyCompatible);
351            // Adjacent major: backward compatible
352            if m > 0 {
353                matrix.set(m - 1, m, CompatibilityLevel::BackwardCompatible);
354                matrix.set(m, m - 1, CompatibilityLevel::BackwardCompatible);
355            }
356        }
357        Self {
358            artifacts: HashMap::new(),
359            compatibility_rules: matrix,
360        }
361    }
362
363    // -----------------------------------------------------------------------
364    // Registration
365    // -----------------------------------------------------------------------
366
367    /// Register a new artifact.
368    ///
369    /// Returns [`SemVerError::ArtifactAlreadyExists`] if an artifact with the
370    /// same ID is already registered.
371    pub fn register_artifact(&mut self, artifact: VersionedArtifact) -> Result<(), SemVerError> {
372        if self.artifacts.contains_key(&artifact.id) {
373            return Err(SemVerError::ArtifactAlreadyExists(artifact.id.clone()));
374        }
375        self.artifacts.insert(artifact.id.clone(), artifact);
376        Ok(())
377    }
378
379    // -----------------------------------------------------------------------
380    // Mutation
381    // -----------------------------------------------------------------------
382
383    /// Apply a change to an artifact, auto-bumping its version according to
384    /// [`ChangeType::required_bump`], appending a [`ChangeRecord`], and
385    /// returning the new version.
386    ///
387    /// Returns [`SemVerError::ArtifactNotFound`] when `artifact_id` is
388    /// unknown.
389    pub fn publish_change(
390        &mut self,
391        artifact_id: &str,
392        change_type: ChangeType,
393        description: String,
394        now: u64,
395    ) -> Result<SemVer, SemVerError> {
396        let artifact = self
397            .artifacts
398            .get_mut(artifact_id)
399            .ok_or_else(|| SemVerError::ArtifactNotFound(artifact_id.to_string()))?;
400
401        let new_version = match change_type.required_bump() {
402            BumpType::Major => artifact.version.bump_major(),
403            BumpType::Minor => artifact.version.bump_minor(),
404            BumpType::Patch => artifact.version.bump_patch(),
405        };
406
407        let record = ChangeRecord {
408            version: new_version.clone(),
409            change_type,
410            description,
411            timestamp: now,
412        };
413
414        artifact.version = new_version.clone();
415        artifact.changelog.push(record);
416        Ok(new_version)
417    }
418
419    // -----------------------------------------------------------------------
420    // Queries
421    // -----------------------------------------------------------------------
422
423    /// Return the current version of an artifact, or `None` if not found.
424    pub fn get_version(&self, artifact_id: &str) -> Option<&SemVer> {
425        self.artifacts.get(artifact_id).map(|a| &a.version)
426    }
427
428    /// Return all changelog entries for an artifact in registration order.
429    pub fn version_history(&self, artifact_id: &str) -> Vec<&ChangeRecord> {
430        self.artifacts
431            .get(artifact_id)
432            .map(|a| a.changelog.iter().collect())
433            .unwrap_or_default()
434    }
435
436    /// Determine the compatibility level between the current versions of two
437    /// artifacts.
438    ///
439    /// Returns errors when either artifact ID is unknown.
440    pub fn check_compatibility(
441        &self,
442        from_id: &str,
443        to_id: &str,
444    ) -> Result<CompatibilityLevel, SemVerError> {
445        let from = self
446            .artifacts
447            .get(from_id)
448            .ok_or_else(|| SemVerError::ArtifactNotFound(from_id.to_string()))?;
449        let to = self
450            .artifacts
451            .get(to_id)
452            .ok_or_else(|| SemVerError::ArtifactNotFound(to_id.to_string()))?;
453
454        Ok(self
455            .compatibility_rules
456            .get(from.version.major, to.version.major))
457    }
458
459    /// Return all changelog entries with [`ChangeType::Breaking`] whose
460    /// version is strictly greater than `since`.
461    pub fn find_breaking_changes(&self, artifact_id: &str, since: &SemVer) -> Vec<&ChangeRecord> {
462        self.artifacts
463            .get(artifact_id)
464            .map(|a| {
465                a.changelog
466                    .iter()
467                    .filter(|r| r.change_type == ChangeType::Breaking && &r.version > since)
468                    .collect()
469            })
470            .unwrap_or_default()
471    }
472
473    /// Compute a migration path between two SemVer values.
474    ///
475    /// Rules:
476    /// * Same version → `[from]`
477    /// * Same major, different minor → `[from, intermediate, to]` where
478    ///   `intermediate` is `(from.major, to.minor, 0)` (i.e. from's major
479    ///   with to's minor bumped in).
480    /// * Adjacent major (|delta| == 1) → `[from, to]`
481    /// * Otherwise → `[]` (incompatible gap, no path)
482    pub fn migration_path(&self, from: &SemVer, to: &SemVer) -> Vec<SemVer> {
483        if from == to {
484            return vec![from.clone()];
485        }
486
487        if from.major == to.major {
488            if from.minor == to.minor {
489                // Only patch differs — direct path
490                return vec![from.clone(), to.clone()];
491            }
492            // Different minor within same major — route through intermediate
493            let intermediate = SemVer::new(from.major, to.minor, 0);
494            return vec![from.clone(), intermediate, to.clone()];
495        }
496
497        let major_delta = (to.major as i64 - from.major as i64).unsigned_abs();
498        if major_delta == 1 {
499            return vec![from.clone(), to.clone()];
500        }
501
502        // Incompatible — no migration path
503        vec![]
504    }
505
506    /// Return the IDs of all artifacts whose current version equals `version`.
507    pub fn artifacts_at_version(&self, version: &SemVer) -> Vec<&str> {
508        self.artifacts
509            .values()
510            .filter(|a| &a.version == version)
511            .map(|a| a.id.as_str())
512            .collect()
513    }
514
515    // -----------------------------------------------------------------------
516    // Statistics
517    // -----------------------------------------------------------------------
518
519    /// Compute aggregate statistics over all registered artifacts.
520    pub fn stats(&self) -> VersioningStats {
521        let total_artifacts = self.artifacts.len();
522
523        let mut total_changes = 0usize;
524        let mut breaking_changes = 0usize;
525        let mut latest_version: Option<SemVer> = None;
526
527        // Accumulators for average version computation
528        let mut sum_major = 0u64;
529        let mut sum_minor = 0u64;
530        let mut sum_patch = 0u64;
531
532        for artifact in self.artifacts.values() {
533            let cl = artifact.changelog.len();
534            total_changes += cl;
535            breaking_changes += artifact
536                .changelog
537                .iter()
538                .filter(|r| r.change_type == ChangeType::Breaking)
539                .count();
540
541            sum_major += artifact.version.major as u64;
542            sum_minor += artifact.version.minor as u64;
543            sum_patch += artifact.version.patch as u64;
544
545            match &latest_version {
546                None => latest_version = Some(artifact.version.clone()),
547                Some(current) if artifact.version > *current => {
548                    latest_version = Some(artifact.version.clone());
549                }
550                _ => {}
551            }
552        }
553
554        let avg_version = if total_artifacts == 0 {
555            "0.0.0".to_string()
556        } else {
557            let n = total_artifacts as u64;
558            let avg_maj = (sum_major / n) as u32;
559            let avg_min = (sum_minor / n) as u32;
560            let avg_pat = (sum_patch / n) as u32;
561            SemVer::new(avg_maj, avg_min, avg_pat).to_string()
562        };
563
564        VersioningStats {
565            total_artifacts,
566            total_changes,
567            breaking_changes,
568            avg_version,
569            latest_version,
570        }
571    }
572}
573
574impl Default for SemanticVersioningEngine {
575    fn default() -> Self {
576        Self::new()
577    }
578}
579
580// ---------------------------------------------------------------------------
581// Tests
582// ---------------------------------------------------------------------------
583
584#[cfg(test)]
585mod tests {
586    use crate::semantic_versioning::{
587        BumpType, ChangeRecord, ChangeType, CompatibilityLevel, CompatibilityMatrix, SemVer,
588        SemVerError, SemanticVersioningEngine, VersionedArtifact,
589    };
590
591    // ------------------------------------------------------------------
592    // SemVer::parse
593    // ------------------------------------------------------------------
594
595    #[test]
596    fn parse_simple() {
597        let v = SemVer::parse("1.2.3").expect("valid");
598        assert_eq!((v.major, v.minor, v.patch), (1, 2, 3));
599        assert!(v.pre_release.is_none());
600        assert!(v.build_metadata.is_none());
601    }
602
603    #[test]
604    fn parse_with_pre_release() {
605        let v = SemVer::parse("1.2.3-alpha").expect("valid");
606        assert_eq!((v.major, v.minor, v.patch), (1, 2, 3));
607        assert_eq!(v.pre_release.as_deref(), Some("alpha"));
608        assert!(v.build_metadata.is_none());
609    }
610
611    #[test]
612    fn parse_with_build_metadata() {
613        let v = SemVer::parse("1.2.3+build.1").expect("valid");
614        assert_eq!((v.major, v.minor, v.patch), (1, 2, 3));
615        assert!(v.pre_release.is_none());
616        assert_eq!(v.build_metadata.as_deref(), Some("build.1"));
617    }
618
619    #[test]
620    fn parse_with_pre_and_build() {
621        let v = SemVer::parse("1.2.3-alpha+build").expect("valid");
622        assert_eq!((v.major, v.minor, v.patch), (1, 2, 3));
623        assert_eq!(v.pre_release.as_deref(), Some("alpha"));
624        assert_eq!(v.build_metadata.as_deref(), Some("build"));
625    }
626
627    #[test]
628    fn parse_zero_version() {
629        let v = SemVer::parse("0.0.0").expect("valid");
630        assert_eq!((v.major, v.minor, v.patch), (0, 0, 0));
631    }
632
633    #[test]
634    fn parse_error_missing_parts() {
635        assert!(matches!(
636            SemVer::parse("1.2"),
637            Err(SemVerError::ParseError(_))
638        ));
639    }
640
641    #[test]
642    fn parse_error_non_numeric() {
643        assert!(matches!(
644            SemVer::parse("a.b.c"),
645            Err(SemVerError::ParseError(_))
646        ));
647    }
648
649    #[test]
650    fn parse_error_extra_dots() {
651        assert!(matches!(
652            SemVer::parse("1.2.3.4"),
653            Err(SemVerError::ParseError(_))
654        ));
655    }
656
657    // ------------------------------------------------------------------
658    // SemVer::Display
659    // ------------------------------------------------------------------
660
661    #[test]
662    fn display_simple() {
663        assert_eq!(SemVer::new(1, 2, 3).to_string(), "1.2.3");
664    }
665
666    #[test]
667    fn display_with_pre() {
668        let v = SemVer::parse("2.0.0-beta").expect("valid");
669        assert_eq!(v.to_string(), "2.0.0-beta");
670    }
671
672    #[test]
673    fn display_with_build() {
674        let v = SemVer::parse("2.0.0+sha.abc").expect("valid");
675        assert_eq!(v.to_string(), "2.0.0+sha.abc");
676    }
677
678    #[test]
679    fn display_with_pre_and_build() {
680        let v = SemVer::parse("2.0.0-rc.1+exp.sha.5114f85").expect("valid");
681        assert_eq!(v.to_string(), "2.0.0-rc.1+exp.sha.5114f85");
682    }
683
684    // ------------------------------------------------------------------
685    // SemVer ordering (ignores pre-release/build)
686    // ------------------------------------------------------------------
687
688    #[test]
689    fn ordering_major() {
690        assert!(SemVer::new(2, 0, 0) > SemVer::new(1, 9, 9));
691    }
692
693    #[test]
694    fn ordering_minor() {
695        assert!(SemVer::new(1, 5, 0) > SemVer::new(1, 4, 99));
696    }
697
698    #[test]
699    fn ordering_patch() {
700        assert!(SemVer::new(1, 0, 2) > SemVer::new(1, 0, 1));
701    }
702
703    #[test]
704    fn ordering_equal() {
705        let a = SemVer::parse("1.2.3-alpha").expect("valid");
706        let b = SemVer::parse("1.2.3+build").expect("valid");
707        assert_eq!(a, b); // pre/build ignored
708    }
709
710    // ------------------------------------------------------------------
711    // SemVer bumps
712    // ------------------------------------------------------------------
713
714    #[test]
715    fn bump_major_resets_minor_patch() {
716        let v = SemVer::new(1, 5, 3).bump_major();
717        assert_eq!((v.major, v.minor, v.patch), (2, 0, 0));
718    }
719
720    #[test]
721    fn bump_minor_resets_patch() {
722        let v = SemVer::new(1, 5, 3).bump_minor();
723        assert_eq!((v.major, v.minor, v.patch), (1, 6, 0));
724    }
725
726    #[test]
727    fn bump_patch_increments() {
728        let v = SemVer::new(1, 5, 3).bump_patch();
729        assert_eq!((v.major, v.minor, v.patch), (1, 5, 4));
730    }
731
732    #[test]
733    fn bump_clears_pre_release() {
734        let v = SemVer::parse("1.0.0-alpha").expect("valid");
735        let bumped = v.bump_patch();
736        assert!(bumped.pre_release.is_none());
737    }
738
739    // ------------------------------------------------------------------
740    // is_compatible_with
741    // ------------------------------------------------------------------
742
743    #[test]
744    fn compatible_same_major_newer() {
745        let a = SemVer::new(1, 5, 0);
746        let b = SemVer::new(1, 3, 0);
747        assert!(a.is_compatible_with(&b));
748    }
749
750    #[test]
751    fn not_compatible_different_major() {
752        let a = SemVer::new(2, 0, 0);
753        let b = SemVer::new(1, 9, 0);
754        assert!(!a.is_compatible_with(&b));
755    }
756
757    #[test]
758    fn not_compatible_older_than_target() {
759        let a = SemVer::new(1, 1, 0);
760        let b = SemVer::new(1, 5, 0);
761        assert!(!a.is_compatible_with(&b));
762    }
763
764    // ------------------------------------------------------------------
765    // ChangeType::required_bump
766    // ------------------------------------------------------------------
767
768    #[test]
769    fn change_type_bump_breaking() {
770        assert_eq!(ChangeType::Breaking.required_bump(), BumpType::Major);
771    }
772
773    #[test]
774    fn change_type_bump_feature() {
775        assert_eq!(ChangeType::Feature.required_bump(), BumpType::Minor);
776    }
777
778    #[test]
779    fn change_type_bump_fix() {
780        assert_eq!(ChangeType::Fix.required_bump(), BumpType::Patch);
781    }
782
783    #[test]
784    fn change_type_bump_documentation() {
785        assert_eq!(ChangeType::Documentation.required_bump(), BumpType::Patch);
786    }
787
788    #[test]
789    fn change_type_bump_refactor() {
790        assert_eq!(ChangeType::Refactor.required_bump(), BumpType::Patch);
791    }
792
793    // ------------------------------------------------------------------
794    // VersionedArtifact::fnv1a
795    // ------------------------------------------------------------------
796
797    #[test]
798    fn fnv1a_empty() {
799        // FNV-1a of empty bytes == FNV offset basis
800        assert_eq!(VersionedArtifact::fnv1a(b""), 14_695_981_039_346_656_037);
801    }
802
803    #[test]
804    fn fnv1a_deterministic() {
805        let h1 = VersionedArtifact::fnv1a(b"hello world");
806        let h2 = VersionedArtifact::fnv1a(b"hello world");
807        assert_eq!(h1, h2);
808    }
809
810    #[test]
811    fn fnv1a_different_inputs() {
812        let h1 = VersionedArtifact::fnv1a(b"foo");
813        let h2 = VersionedArtifact::fnv1a(b"bar");
814        assert_ne!(h1, h2);
815    }
816
817    // ------------------------------------------------------------------
818    // CompatibilityMatrix
819    // ------------------------------------------------------------------
820
821    #[test]
822    fn matrix_default_incompatible() {
823        let m = CompatibilityMatrix::default();
824        assert_eq!(m.get(0, 5), CompatibilityLevel::Incompatible);
825    }
826
827    #[test]
828    fn matrix_set_and_get() {
829        let mut m = CompatibilityMatrix::default();
830        m.set(1, 2, CompatibilityLevel::BackwardCompatible);
831        assert_eq!(m.get(1, 2), CompatibilityLevel::BackwardCompatible);
832    }
833
834    // ------------------------------------------------------------------
835    // SemanticVersioningEngine::register_artifact
836    // ------------------------------------------------------------------
837
838    fn make_artifact(id: &str) -> VersionedArtifact {
839        VersionedArtifact {
840            id: id.to_string(),
841            version: SemVer::new(0, 1, 0),
842            content_hash: 0,
843            embedding_dim: None,
844            created_at: 1_000,
845            changelog: vec![],
846        }
847    }
848
849    #[test]
850    fn register_new_artifact() {
851        let mut engine = SemanticVersioningEngine::new();
852        assert!(engine.register_artifact(make_artifact("doc-a")).is_ok());
853        assert!(engine.get_version("doc-a").is_some());
854    }
855
856    #[test]
857    fn register_duplicate_returns_error() {
858        let mut engine = SemanticVersioningEngine::new();
859        engine
860            .register_artifact(make_artifact("doc-a"))
861            .expect("first");
862        let err = engine
863            .register_artifact(make_artifact("doc-a"))
864            .unwrap_err();
865        assert!(matches!(err, SemVerError::ArtifactAlreadyExists(_)));
866    }
867
868    // ------------------------------------------------------------------
869    // publish_change
870    // ------------------------------------------------------------------
871
872    #[test]
873    fn publish_breaking_bumps_major() {
874        let mut engine = SemanticVersioningEngine::new();
875        engine.register_artifact(make_artifact("a")).expect("ok");
876        let v = engine
877            .publish_change("a", ChangeType::Breaking, "Removed old API".into(), 2000)
878            .expect("ok");
879        assert_eq!(v, SemVer::new(1, 0, 0));
880    }
881
882    #[test]
883    fn publish_feature_bumps_minor() {
884        let mut engine = SemanticVersioningEngine::new();
885        engine.register_artifact(make_artifact("a")).expect("ok");
886        let v = engine
887            .publish_change("a", ChangeType::Feature, "New endpoint".into(), 2000)
888            .expect("ok");
889        assert_eq!(v, SemVer::new(0, 2, 0));
890    }
891
892    #[test]
893    fn publish_fix_bumps_patch() {
894        let mut engine = SemanticVersioningEngine::new();
895        engine.register_artifact(make_artifact("a")).expect("ok");
896        let v = engine
897            .publish_change("a", ChangeType::Fix, "Null ptr fix".into(), 2000)
898            .expect("ok");
899        assert_eq!(v, SemVer::new(0, 1, 1));
900    }
901
902    #[test]
903    fn publish_unknown_artifact_errors() {
904        let mut engine = SemanticVersioningEngine::new();
905        let err = engine
906            .publish_change("missing", ChangeType::Fix, "x".into(), 0)
907            .unwrap_err();
908        assert!(matches!(err, SemVerError::ArtifactNotFound(_)));
909    }
910
911    #[test]
912    fn publish_appends_changelog() {
913        let mut engine = SemanticVersioningEngine::new();
914        engine.register_artifact(make_artifact("a")).expect("ok");
915        engine
916            .publish_change("a", ChangeType::Feature, "feat 1".into(), 1000)
917            .expect("ok");
918        engine
919            .publish_change("a", ChangeType::Fix, "fix 1".into(), 2000)
920            .expect("ok");
921        let history = engine.version_history("a");
922        assert_eq!(history.len(), 2);
923    }
924
925    // ------------------------------------------------------------------
926    // version_history
927    // ------------------------------------------------------------------
928
929    #[test]
930    fn history_of_unknown_artifact_is_empty() {
931        let engine = SemanticVersioningEngine::new();
932        assert!(engine.version_history("ghost").is_empty());
933    }
934
935    // ------------------------------------------------------------------
936    // check_compatibility
937    // ------------------------------------------------------------------
938
939    #[test]
940    fn same_major_fully_compatible() {
941        let mut engine = SemanticVersioningEngine::new();
942        engine.register_artifact(make_artifact("a")).expect("ok");
943        engine.register_artifact(make_artifact("b")).expect("ok");
944        assert_eq!(
945            engine.check_compatibility("a", "b").expect("ok"),
946            CompatibilityLevel::FullyCompatible
947        );
948    }
949
950    #[test]
951    fn adjacent_major_backward_compatible() {
952        let mut engine = SemanticVersioningEngine::new();
953        // Bump "b" to major 1
954        engine.register_artifact(make_artifact("a")).expect("ok");
955        let mut b = make_artifact("b");
956        b.version = SemVer::new(1, 0, 0);
957        engine.register_artifact(b).expect("ok");
958        assert_eq!(
959            engine.check_compatibility("a", "b").expect("ok"),
960            CompatibilityLevel::BackwardCompatible
961        );
962    }
963
964    #[test]
965    fn check_compatibility_unknown_from() {
966        let mut engine = SemanticVersioningEngine::new();
967        engine.register_artifact(make_artifact("b")).expect("ok");
968        assert!(matches!(
969            engine.check_compatibility("ghost", "b"),
970            Err(SemVerError::ArtifactNotFound(_))
971        ));
972    }
973
974    #[test]
975    fn check_compatibility_unknown_to() {
976        let mut engine = SemanticVersioningEngine::new();
977        engine.register_artifact(make_artifact("a")).expect("ok");
978        assert!(matches!(
979            engine.check_compatibility("a", "ghost"),
980            Err(SemVerError::ArtifactNotFound(_))
981        ));
982    }
983
984    // ------------------------------------------------------------------
985    // find_breaking_changes
986    // ------------------------------------------------------------------
987
988    #[test]
989    fn find_breaking_changes_empty_when_no_breaking() {
990        let mut engine = SemanticVersioningEngine::new();
991        engine.register_artifact(make_artifact("a")).expect("ok");
992        engine
993            .publish_change("a", ChangeType::Feature, "f".into(), 100)
994            .expect("ok");
995        let bc = engine.find_breaking_changes("a", &SemVer::new(0, 1, 0));
996        assert!(bc.is_empty());
997    }
998
999    #[test]
1000    fn find_breaking_changes_returns_breaking_after_since() {
1001        let mut engine = SemanticVersioningEngine::new();
1002        engine.register_artifact(make_artifact("a")).expect("ok");
1003        let since = engine.get_version("a").expect("ok").clone();
1004        engine
1005            .publish_change("a", ChangeType::Breaking, "removed X".into(), 100)
1006            .expect("ok");
1007        let bc = engine.find_breaking_changes("a", &since);
1008        assert_eq!(bc.len(), 1);
1009    }
1010
1011    #[test]
1012    fn find_breaking_changes_not_included_at_or_before_since() {
1013        let mut engine = SemanticVersioningEngine::new();
1014        engine.register_artifact(make_artifact("a")).expect("ok");
1015        engine
1016            .publish_change("a", ChangeType::Breaking, "removed X".into(), 100)
1017            .expect("ok");
1018        let since = engine.get_version("a").expect("ok").clone();
1019        // No new breaking changes after `since`
1020        let bc = engine.find_breaking_changes("a", &since);
1021        assert!(bc.is_empty());
1022    }
1023
1024    // ------------------------------------------------------------------
1025    // migration_path
1026    // ------------------------------------------------------------------
1027
1028    #[test]
1029    fn migration_path_same_version() {
1030        let engine = SemanticVersioningEngine::new();
1031        let v = SemVer::new(1, 2, 3);
1032        let path = engine.migration_path(&v, &v);
1033        assert_eq!(path, vec![v]);
1034    }
1035
1036    #[test]
1037    fn migration_path_same_major_different_minor() {
1038        let engine = SemanticVersioningEngine::new();
1039        let from = SemVer::new(1, 1, 0);
1040        let to = SemVer::new(1, 4, 0);
1041        let path = engine.migration_path(&from, &to);
1042        assert_eq!(path.len(), 3);
1043        assert_eq!(path[1], SemVer::new(1, 4, 0)); // intermediate with to's minor
1044    }
1045
1046    #[test]
1047    fn migration_path_adjacent_major() {
1048        let engine = SemanticVersioningEngine::new();
1049        let from = SemVer::new(1, 5, 0);
1050        let to = SemVer::new(2, 0, 0);
1051        let path = engine.migration_path(&from, &to);
1052        assert_eq!(path, vec![from, to]);
1053    }
1054
1055    #[test]
1056    fn migration_path_incompatible_gap() {
1057        let engine = SemanticVersioningEngine::new();
1058        let from = SemVer::new(1, 0, 0);
1059        let to = SemVer::new(5, 0, 0);
1060        let path = engine.migration_path(&from, &to);
1061        assert!(path.is_empty());
1062    }
1063
1064    #[test]
1065    fn migration_path_same_minor_different_patch() {
1066        let engine = SemanticVersioningEngine::new();
1067        let from = SemVer::new(2, 3, 0);
1068        let to = SemVer::new(2, 3, 5);
1069        let path = engine.migration_path(&from, &to);
1070        assert_eq!(path, vec![from, to]);
1071    }
1072
1073    // ------------------------------------------------------------------
1074    // artifacts_at_version
1075    // ------------------------------------------------------------------
1076
1077    #[test]
1078    fn artifacts_at_version_matches() {
1079        let mut engine = SemanticVersioningEngine::new();
1080        engine.register_artifact(make_artifact("a")).expect("ok");
1081        engine.register_artifact(make_artifact("b")).expect("ok");
1082        let mut ids = engine.artifacts_at_version(&SemVer::new(0, 1, 0));
1083        ids.sort_unstable();
1084        assert_eq!(ids, vec!["a", "b"]);
1085    }
1086
1087    #[test]
1088    fn artifacts_at_version_empty_when_none_match() {
1089        let mut engine = SemanticVersioningEngine::new();
1090        engine.register_artifact(make_artifact("a")).expect("ok");
1091        let ids = engine.artifacts_at_version(&SemVer::new(9, 9, 9));
1092        assert!(ids.is_empty());
1093    }
1094
1095    // ------------------------------------------------------------------
1096    // stats
1097    // ------------------------------------------------------------------
1098
1099    #[test]
1100    fn stats_empty_engine() {
1101        let engine = SemanticVersioningEngine::new();
1102        let s = engine.stats();
1103        assert_eq!(s.total_artifacts, 0);
1104        assert_eq!(s.total_changes, 0);
1105        assert_eq!(s.breaking_changes, 0);
1106        assert_eq!(s.avg_version, "0.0.0");
1107        assert!(s.latest_version.is_none());
1108    }
1109
1110    #[test]
1111    fn stats_single_artifact_no_changes() {
1112        let mut engine = SemanticVersioningEngine::new();
1113        engine.register_artifact(make_artifact("a")).expect("ok");
1114        let s = engine.stats();
1115        assert_eq!(s.total_artifacts, 1);
1116        assert_eq!(s.total_changes, 0);
1117        assert_eq!(s.breaking_changes, 0);
1118        assert_eq!(s.latest_version, Some(SemVer::new(0, 1, 0)));
1119    }
1120
1121    #[test]
1122    fn stats_counts_breaking_changes() {
1123        let mut engine = SemanticVersioningEngine::new();
1124        engine.register_artifact(make_artifact("a")).expect("ok");
1125        engine
1126            .publish_change("a", ChangeType::Breaking, "b1".into(), 100)
1127            .expect("ok");
1128        engine
1129            .publish_change("a", ChangeType::Feature, "f1".into(), 200)
1130            .expect("ok");
1131        let s = engine.stats();
1132        assert_eq!(s.total_changes, 2);
1133        assert_eq!(s.breaking_changes, 1);
1134    }
1135
1136    #[test]
1137    fn stats_latest_version_is_max() {
1138        let mut engine = SemanticVersioningEngine::new();
1139        engine.register_artifact(make_artifact("a")).expect("ok");
1140        let mut b = make_artifact("b");
1141        b.version = SemVer::new(3, 0, 0);
1142        engine.register_artifact(b).expect("ok");
1143        let s = engine.stats();
1144        assert_eq!(s.latest_version, Some(SemVer::new(3, 0, 0)));
1145    }
1146
1147    #[test]
1148    fn stats_avg_version_two_artifacts() {
1149        let mut engine = SemanticVersioningEngine::new();
1150        // versions 0.1.0 and 2.3.4 → avg = 1.2.2
1151        engine.register_artifact(make_artifact("a")).expect("ok");
1152        let mut b = make_artifact("b");
1153        b.version = SemVer::new(2, 3, 4);
1154        engine.register_artifact(b).expect("ok");
1155        let s = engine.stats();
1156        // floor(0+2/2)=1, floor(1+3/2)=2, floor(0+4/2)=2
1157        assert_eq!(s.avg_version, "1.2.2");
1158    }
1159
1160    // ------------------------------------------------------------------
1161    // ChangeRecord presence in history
1162    // ------------------------------------------------------------------
1163
1164    #[test]
1165    fn change_record_fields_preserved() {
1166        let mut engine = SemanticVersioningEngine::new();
1167        engine.register_artifact(make_artifact("a")).expect("ok");
1168        engine
1169            .publish_change(
1170                "a",
1171                ChangeType::Documentation,
1172                "Update readme".into(),
1173                42_000,
1174            )
1175            .expect("ok");
1176        let history = engine.version_history("a");
1177        let rec: &ChangeRecord = history[0];
1178        assert_eq!(rec.change_type, ChangeType::Documentation);
1179        assert_eq!(rec.description, "Update readme");
1180        assert_eq!(rec.timestamp, 42_000);
1181    }
1182
1183    // ------------------------------------------------------------------
1184    // Default impl
1185    // ------------------------------------------------------------------
1186
1187    #[test]
1188    fn default_engine_is_empty() {
1189        let engine = SemanticVersioningEngine::default();
1190        assert!(engine.artifacts.is_empty());
1191    }
1192
1193    // ------------------------------------------------------------------
1194    // SemVerError Display
1195    // ------------------------------------------------------------------
1196
1197    #[test]
1198    fn error_display_parse() {
1199        let e = SemVerError::ParseError("bad".into());
1200        assert!(e.to_string().contains("bad"));
1201    }
1202
1203    #[test]
1204    fn error_display_not_found() {
1205        let e = SemVerError::ArtifactNotFound("x".into());
1206        assert!(e.to_string().contains("x"));
1207    }
1208
1209    #[test]
1210    fn error_display_already_exists() {
1211        let e = SemVerError::ArtifactAlreadyExists("x".into());
1212        assert!(e.to_string().contains("x"));
1213    }
1214
1215    #[test]
1216    fn error_display_invalid_version() {
1217        let e = SemVerError::InvalidVersion("0.0.0".into());
1218        assert!(e.to_string().contains("0.0.0"));
1219    }
1220}