- Fixed systematic array indexing corruption: [0_i32] → [0] - Fixed numeric literal suffixes across 835 files - Fixed iterator patterns on RwLockReadGuard (.iter() required) - Fixed float type annotations (365.25_f64 for sqrt) - Fixed missing semicolons in position manager - Fixed reference dereferencing in data loader Root cause: Mass refactoring incorrectly added _i32 suffixes to array indices Impact: Complete compilation failure (463 errors) Resolution: Automated regex + targeted fixes Result: 100% compilation success (0 errors) Validated: cargo check --workspace passes Ready for: Production deployment
544 lines
17 KiB
Rust
544 lines
17 KiB
Rust
//! Semantic Versioning System for ML Models
|
|
//!
|
|
//! Implements semantic versioning with compatibility checking, migration paths,
|
|
//! and version comparison utilities for ML model deployments.
|
|
|
|
use std::cmp::Ordering;
|
|
use std::collections::HashMap;
|
|
use std::fmt;
|
|
use std::time::SystemTime;
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use crate::MLError;
|
|
|
|
/// Semantic version for ML models
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
pub struct ModelVersion {
|
|
/// Major version (breaking changes)
|
|
pub major: u32,
|
|
/// Minor version (backward-compatible features)
|
|
pub minor: u32,
|
|
/// Patch version (bug fixes)
|
|
pub patch: u32,
|
|
/// Pre-release identifier (alpha, beta, rc)
|
|
pub pre_release: Option<String>,
|
|
/// Build metadata
|
|
pub build: Option<String>,
|
|
}
|
|
|
|
impl ModelVersion {
|
|
/// Create a new semantic version
|
|
pub fn new(major: u32, minor: u32, patch: u32) -> Self {
|
|
Self {
|
|
major,
|
|
minor,
|
|
patch,
|
|
pre_release: None,
|
|
build: None,
|
|
}
|
|
}
|
|
|
|
/// Create version with pre-release identifier
|
|
pub fn new_pre_release(major: u32, minor: u32, patch: u32, pre_release: String) -> Self {
|
|
Self {
|
|
major,
|
|
minor,
|
|
patch,
|
|
pre_release: Some(pre_release),
|
|
build: None,
|
|
}
|
|
}
|
|
|
|
/// Create version with build metadata
|
|
pub fn new_with_build(major: u32, minor: u32, patch: u32, build: String) -> Self {
|
|
Self {
|
|
major,
|
|
minor,
|
|
patch,
|
|
pre_release: None,
|
|
build: Some(build),
|
|
}
|
|
}
|
|
|
|
/// Parse version from string (e.g., "1.2.3-alpha+build.1")
|
|
pub fn parse(version_str: &str) -> Result<Self, MLError> {
|
|
let parts: Vec<&str> = version_str.split('+').collect();
|
|
let (version_part, build) = match parts.len() {
|
|
1 => (parts[0], None),
|
|
2 => (parts[0], Some(parts[1].to_string())),
|
|
_ => return Err(MLError::ValidationError {
|
|
message: format!("Invalid version format: {}", version_str),
|
|
}),
|
|
};
|
|
|
|
let parts: Vec<&str> = version_part.split('-').collect();
|
|
let (core_version, pre_release) = match parts.len() {
|
|
1 => (parts[0], None),
|
|
2 => (parts[0], Some(parts[1].to_string())),
|
|
_ => return Err(MLError::ValidationError {
|
|
message: format!("Invalid version format: {}", version_str),
|
|
}),
|
|
};
|
|
|
|
let version_numbers: Vec<&str> = core_version.split('.').collect();
|
|
if version_numbers.len() != 3 {
|
|
return Err(MLError::ValidationError {
|
|
message: format!("Version must have three numbers: {}", version_str),
|
|
});
|
|
}
|
|
|
|
let major = version_numbers[0].parse::<u32>().map_err(|_| MLError::ValidationError {
|
|
message: format!("Invalid major version: {}", version_numbers[0]),
|
|
})?;
|
|
|
|
let minor = version_numbers[1].parse::<u32>().map_err(|_| MLError::ValidationError {
|
|
message: format!("Invalid minor version: {}", version_numbers[1]),
|
|
})?;
|
|
|
|
let patch = version_numbers[2].parse::<u32>().map_err(|_| MLError::ValidationError {
|
|
message: format!("Invalid patch version: {}", version_numbers[2]),
|
|
})?;
|
|
|
|
Ok(Self {
|
|
major,
|
|
minor,
|
|
patch,
|
|
pre_release,
|
|
build,
|
|
})
|
|
}
|
|
|
|
/// Check if this version is compatible with another version
|
|
pub fn is_compatible_with(&self, other: &ModelVersion) -> bool {
|
|
// Major version must match for compatibility
|
|
if self.major != other.major {
|
|
return false;
|
|
}
|
|
|
|
// For the same major version, newer minor/patch versions are backward compatible
|
|
match (self.minor.cmp(&other.minor), self.patch.cmp(&other.patch)) {
|
|
(Ordering::Greater, _) => true,
|
|
(Ordering::Equal, Ordering::Greater | Ordering::Equal) => true,
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
/// Check if this version represents a breaking change from another version
|
|
pub fn is_breaking_change(&self, other: &ModelVersion) -> bool {
|
|
self.major > other.major
|
|
}
|
|
|
|
/// Check if this version represents a feature addition from another version
|
|
pub fn is_feature_addition(&self, other: &ModelVersion) -> bool {
|
|
self.major == other.major && self.minor > other.minor
|
|
}
|
|
|
|
/// Check if this version represents a bug fix from another version
|
|
pub fn is_bug_fix(&self, other: &ModelVersion) -> bool {
|
|
self.major == other.major && self.minor == other.minor && self.patch > other.patch
|
|
}
|
|
|
|
/// Get the next major version
|
|
pub fn next_major(&self) -> Self {
|
|
Self::new(self.major + 1, 0, 0)
|
|
}
|
|
|
|
/// Get the next minor version
|
|
pub fn next_minor(&self) -> Self {
|
|
Self::new(self.major, self.minor + 1, 0)
|
|
}
|
|
|
|
/// Get the next patch version
|
|
pub fn next_patch(&self) -> Self {
|
|
Self::new(self.major, self.minor, self.patch + 1)
|
|
}
|
|
|
|
/// Check if this is a pre-release version
|
|
pub fn is_pre_release(&self) -> bool {
|
|
self.pre_release.is_some()
|
|
}
|
|
|
|
/// Check if this is a stable release
|
|
pub fn is_stable(&self) -> bool {
|
|
!self.is_pre_release()
|
|
}
|
|
|
|
/// Get version string without build metadata
|
|
pub fn version_string(&self) -> String {
|
|
let mut version = format!("{}.{}.{}", self.major, self.minor, self.patch);
|
|
if let Some(ref pre) = self.pre_release {
|
|
version.push('-');
|
|
version.push_str(pre);
|
|
}
|
|
version
|
|
}
|
|
|
|
/// Get full version string including build metadata
|
|
pub fn full_version_string(&self) -> String {
|
|
let mut version = self.version_string();
|
|
if let Some(ref build) = self.build {
|
|
version.push('+');
|
|
version.push_str(build);
|
|
}
|
|
version
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for ModelVersion {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
write!(f, "{}", self.full_version_string())
|
|
}
|
|
}
|
|
|
|
impl PartialOrd for ModelVersion {
|
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
|
Some(self.cmp(other))
|
|
}
|
|
}
|
|
|
|
impl Ord for ModelVersion {
|
|
fn cmp(&self, other: &Self) -> Ordering {
|
|
// Compare major version first
|
|
match self.major.cmp(&other.major) {
|
|
Ordering::Equal => {}
|
|
other => return other,
|
|
}
|
|
|
|
// Compare minor version
|
|
match self.minor.cmp(&other.minor) {
|
|
Ordering::Equal => {}
|
|
other => return other,
|
|
}
|
|
|
|
// Compare patch version
|
|
match self.patch.cmp(&other.patch) {
|
|
Ordering::Equal => {}
|
|
other => return other,
|
|
}
|
|
|
|
// Compare pre-release versions
|
|
match (&self.pre_release, &other.pre_release) {
|
|
(None, None) => Ordering::Equal,
|
|
(None, Some(_)) => Ordering::Greater, // Release > Pre-release
|
|
(Some(_), None) => Ordering::Less, // Pre-release < Release
|
|
(Some(a), Some(b)) => a.cmp(b),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Version constraints for model dependencies
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub enum VersionConstraint {
|
|
/// Exact version match
|
|
Exact(ModelVersion),
|
|
/// Minimum version (inclusive)
|
|
AtLeast(ModelVersion),
|
|
/// Maximum version (exclusive)
|
|
Below(ModelVersion),
|
|
/// Range (inclusive start, exclusive end)
|
|
Range(ModelVersion, ModelVersion),
|
|
/// Compatible (same major version, at least the specified version)
|
|
Compatible(ModelVersion),
|
|
}
|
|
|
|
impl VersionConstraint {
|
|
/// Check if a version satisfies this constraint
|
|
pub fn satisfies(&self, version: &ModelVersion) -> bool {
|
|
match self {
|
|
VersionConstraint::Exact(target) => version == target,
|
|
VersionConstraint::AtLeast(min) => version >= min,
|
|
VersionConstraint::Below(max) => version < max,
|
|
VersionConstraint::Range(min, max) => version >= min && version < max,
|
|
VersionConstraint::Compatible(base) => {
|
|
version.major == base.major && version >= base
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Version history tracking
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct VersionHistory {
|
|
/// All versions in chronological order
|
|
pub versions: Vec<VersionEntry>,
|
|
/// Current active version
|
|
pub current_version: Option<ModelVersion>,
|
|
}
|
|
|
|
/// Entry in version history
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct VersionEntry {
|
|
/// Version information
|
|
pub version: ModelVersion,
|
|
/// Deployment timestamp
|
|
pub deployed_at: std::time::SystemTime,
|
|
/// Change description
|
|
pub change_description: String,
|
|
/// Deployment author
|
|
pub deployed_by: String,
|
|
/// Performance metrics at deployment
|
|
pub performance_metrics: HashMap<String, f64>,
|
|
/// Migration notes
|
|
pub migration_notes: Option<String>,
|
|
}
|
|
|
|
impl VersionHistory {
|
|
/// Create new version history
|
|
pub fn new() -> Self {
|
|
Self {
|
|
versions: Vec::new(),
|
|
current_version: None,
|
|
}
|
|
}
|
|
|
|
/// Add new version to history
|
|
pub fn add_version(&mut self, entry: VersionEntry) -> Result<(), MLError> {
|
|
// Validate that new version is higher than current
|
|
if let Some(ref current) = self.current_version {
|
|
if entry.version <= *current {
|
|
return Err(MLError::ValidationError {
|
|
message: format!(
|
|
"New version {} must be higher than current version {}",
|
|
entry.version, current
|
|
),
|
|
});
|
|
}
|
|
}
|
|
|
|
self.versions.push(entry.clone());
|
|
self.current_version = Some(entry.version);
|
|
|
|
// Sort versions to maintain chronological order
|
|
self.versions.sort_by(|a, b| a.version.cmp(&b.version));
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Get version entry by version
|
|
pub fn get_version(&self, version: &ModelVersion) -> Option<&VersionEntry> {
|
|
self.versions.iter().find(|entry| &entry.version == version)
|
|
}
|
|
|
|
/// Get all versions that satisfy a constraint
|
|
pub fn get_versions_satisfying(&self, constraint: &VersionConstraint) -> Vec<&VersionEntry> {
|
|
self.versions
|
|
.iter()
|
|
.filter(|entry| constraint.satisfies(&entry.version))
|
|
.collect()
|
|
}
|
|
|
|
/// Get the latest stable version
|
|
pub fn get_latest_stable(&self) -> Option<&VersionEntry> {
|
|
self.versions
|
|
.iter()
|
|
.rev()
|
|
.find(|entry| entry.version.is_stable())
|
|
}
|
|
|
|
/// Get all breaking changes from a base version
|
|
pub fn get_breaking_changes(&self, base_version: &ModelVersion) -> Vec<&VersionEntry> {
|
|
self.versions
|
|
.iter()
|
|
.filter(|entry| entry.version.is_breaking_change(base_version))
|
|
.collect()
|
|
}
|
|
|
|
/// Calculate migration path between versions
|
|
pub fn calculate_migration_path(
|
|
&self,
|
|
from: &ModelVersion,
|
|
to: &ModelVersion,
|
|
) -> Result<Vec<&VersionEntry>, MLError> {
|
|
if from > to {
|
|
return Err(MLError::ValidationError {
|
|
message: "Cannot migrate to an older version".to_string(),
|
|
});
|
|
}
|
|
|
|
let path: Vec<&VersionEntry> = self
|
|
.versions
|
|
.iter()
|
|
.filter(|entry| &entry.version > from && &entry.version <= to)
|
|
.collect();
|
|
|
|
if path.is_empty() {
|
|
return Err(MLError::ValidationError {
|
|
message: format!("No migration path found from {} to {}", from, to),
|
|
});
|
|
}
|
|
|
|
Ok(path)
|
|
}
|
|
}
|
|
|
|
impl Default for VersionHistory {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
/// Version comparison utilities
|
|
pub mod version_utils {
|
|
use super::*;
|
|
|
|
/// Find the highest compatible version from a list
|
|
pub fn find_highest_compatible(
|
|
versions: &[ModelVersion],
|
|
constraint: &VersionConstraint,
|
|
) -> Option<&ModelVersion> {
|
|
versions
|
|
.iter()
|
|
.filter(|version| constraint.satisfies(version))
|
|
.max()
|
|
}
|
|
|
|
/// Check if an upgrade is safe (no breaking changes)
|
|
pub fn is_safe_upgrade(from: &ModelVersion, to: &ModelVersion) -> bool {
|
|
to.is_compatible_with(from) && !to.is_breaking_change(from)
|
|
}
|
|
|
|
/// Generate version recommendations
|
|
pub fn recommend_next_version(
|
|
current: &ModelVersion,
|
|
change_type: ChangeType,
|
|
) -> ModelVersion {
|
|
match change_type {
|
|
ChangeType::BreakingChange => current.next_major(),
|
|
ChangeType::Feature => current.next_minor(),
|
|
ChangeType::BugFix => current.next_patch(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Type of change for version recommendation
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum ChangeType {
|
|
/// Breaking API or behavior change
|
|
BreakingChange,
|
|
/// New feature or enhancement
|
|
Feature,
|
|
/// Bug fix or patch
|
|
BugFix,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_version_creation() {
|
|
let version = ModelVersion::new(1, 2, 3);
|
|
assert_eq!(version.major, 1);
|
|
assert_eq!(version.minor, 2);
|
|
assert_eq!(version.patch, 3);
|
|
assert!(version.pre_release.is_none());
|
|
assert!(version.build.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_version_parsing() {
|
|
let version = ModelVersion::parse("1.2.3-alpha+build.1").unwrap();
|
|
assert_eq!(version.major, 1);
|
|
assert_eq!(version.minor, 2);
|
|
assert_eq!(version.patch, 3);
|
|
assert_eq!(version.pre_release, Some("alpha".to_string()));
|
|
assert_eq!(version.build, Some("build.1".to_string()));
|
|
}
|
|
|
|
#[test]
|
|
fn test_version_comparison() {
|
|
let v1 = ModelVersion::new(1, 0, 0);
|
|
let v2 = ModelVersion::new(1, 1, 0);
|
|
let v3 = ModelVersion::new(2, 0, 0);
|
|
|
|
assert!(v2 > v1);
|
|
assert!(v3 > v2);
|
|
assert!(v3 > v1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_compatibility() {
|
|
let v1_0_0 = ModelVersion::new(1, 0, 0);
|
|
let v1_1_0 = ModelVersion::new(1, 1, 0);
|
|
let v1_1_1 = ModelVersion::new(1, 1, 1);
|
|
let v2_0_0 = ModelVersion::new(2, 0, 0);
|
|
|
|
assert!(v1_1_0.is_compatible_with(&v1_0_0));
|
|
assert!(v1_1_1.is_compatible_with(&v1_1_0));
|
|
assert!(!v2_0_0.is_compatible_with(&v1_1_1));
|
|
}
|
|
|
|
#[test]
|
|
fn test_breaking_changes() {
|
|
let v1_0_0 = ModelVersion::new(1, 0, 0);
|
|
let v1_1_0 = ModelVersion::new(1, 1, 0);
|
|
let v2_0_0 = ModelVersion::new(2, 0, 0);
|
|
|
|
assert!(!v1_1_0.is_breaking_change(&v1_0_0));
|
|
assert!(v2_0_0.is_breaking_change(&v1_1_0));
|
|
}
|
|
|
|
#[test]
|
|
fn test_version_constraints() {
|
|
let v1_0_0 = ModelVersion::new(1, 0, 0);
|
|
let v1_1_0 = ModelVersion::new(1, 1, 0);
|
|
let v2_0_0 = ModelVersion::new(2, 0, 0);
|
|
|
|
let constraint = VersionConstraint::Compatible(v1_0_0.clone());
|
|
assert!(constraint.satisfies(&v1_1_0));
|
|
assert!(!constraint.satisfies(&v2_0_0));
|
|
|
|
let range_constraint = VersionConstraint::Range(v1_0_0, v2_0_0.clone());
|
|
assert!(range_constraint.satisfies(&v1_1_0));
|
|
assert!(!range_constraint.satisfies(&v2_0_0));
|
|
}
|
|
|
|
#[test]
|
|
fn test_version_history() {
|
|
let mut history = VersionHistory::new();
|
|
|
|
let entry1 = VersionEntry {
|
|
version: ModelVersion::new(1, 0, 0),
|
|
deployed_at: std::time::SystemTime::now(),
|
|
change_description: "Initial release".to_string(),
|
|
deployed_by: "user1".to_string(),
|
|
performance_metrics: HashMap::new(),
|
|
migration_notes: None,
|
|
};
|
|
|
|
let entry2 = VersionEntry {
|
|
version: ModelVersion::new(1, 1, 0),
|
|
deployed_at: std::time::SystemTime::now(),
|
|
change_description: "Feature addition".to_string(),
|
|
deployed_by: "user2".to_string(),
|
|
performance_metrics: HashMap::new(),
|
|
migration_notes: Some("Migration guide available".to_string()),
|
|
};
|
|
|
|
assert!(history.add_version(entry1).is_ok());
|
|
assert!(history.add_version(entry2).is_ok());
|
|
assert_eq!(history.versions.len(), 2);
|
|
assert_eq!(history.current_version, Some(ModelVersion::new(1, 1, 0)));
|
|
}
|
|
|
|
#[test]
|
|
fn test_version_utils() {
|
|
let versions = vec![
|
|
ModelVersion::new(1, 0, 0),
|
|
ModelVersion::new(1, 1, 0),
|
|
ModelVersion::new(1, 2, 0),
|
|
ModelVersion::new(2, 0, 0),
|
|
];
|
|
|
|
let constraint = VersionConstraint::Compatible(ModelVersion::new(1, 0, 0));
|
|
let highest = version_utils::find_highest_compatible(&versions, &constraint);
|
|
|
|
assert_eq!(highest, Some(&ModelVersion::new(1, 2, 0)));
|
|
|
|
let current = ModelVersion::new(1, 0, 0);
|
|
let next_feature = version_utils::recommend_next_version(¤t, ChangeType::Feature);
|
|
assert_eq!(next_feature, ModelVersion::new(1, 1, 0));
|
|
}
|
|
}
|