Files
foxhunt/ml/src/deployment/versioning.rs
jgrusewski 030a15ee05 🔧 Emergency Fix: Resolve catastrophic _i32 suffix corruption (463→0 errors)
- 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
2025-10-10 23:05:26 +02:00

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(&current, ChangeType::Feature);
assert_eq!(next_feature, ModelVersion::new(1, 1, 0));
}
}