VBAF.ML.ModelRegistry.ps1

#Requires -Version 5.1
<#
.SYNOPSIS
    Model Registry - Save, Load, Version and Track ML Models
.DESCRIPTION
    Implements model persistence and versioning from scratch.
    Designed as a TEACHING resource - every step explained.
    Features included:
      - Save/load trained models : serialize weights + metadata to JSON
      - Model versioning : semantic versioning, auto-increment
      - Metadata tracking : metrics, params, dataset, timestamp
      - Backwards compatibility : schema version checking on load
      - Model registry : central index of all saved models
    Works with all VBAF ML modules (Regression, Trees, CNN, RNN, etc.)
    Standalone - no external VBAF dependencies required.
.NOTES
    Part of VBAF - Phase 7 Production Features - v2.1.0
    PS 5.1 compatible
    Teaching project - MLOps concepts explained step by step!
#>

$basePath = $PSScriptRoot

# ============================================================
# TEACHING NOTE: Why model persistence?
# Training takes time. Once trained, you want to:
# SAVE : preserve the model so you don't retrain every time
# LOAD : restore and use without retraining
# VERSION: track which model is which (v1.0, v1.1, v2.0)
# COMPARE: pick the best version based on metrics
# AUDIT : know exactly what data/params produced each model
#
# This is the foundation of MLOps (ML Operations) -
# treating models like software with proper versioning!
# ============================================================

# Registry storage location
$script:VBAFRegistryPath = Join-Path $env:USERPROFILE "VBAFRegistry"
$script:RegistryIndex    = Join-Path $script:VBAFRegistryPath "registry.json"
$script:SchemaVersion    = "2.1.0"

# ============================================================
# REGISTRY INITIALIZATION
# ============================================================

function Initialize-VBAFRegistry {
    param([string]$Path = $script:VBAFRegistryPath)

    if (-not (Test-Path $Path)) {
        New-Item -ItemType Directory -Path $Path -Force | Out-Null
        Write-Host "📁 Registry created: $Path" -ForegroundColor Green
    }

    $indexPath = Join-Path $Path "registry.json"
    if (-not (Test-Path $indexPath)) {
        $index = @{
            SchemaVersion = $script:SchemaVersion
            Created       = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
            Models        = @{}
        }
        $index | ConvertTo-Json -Depth 5 | Set-Content $indexPath -Encoding UTF8
        Write-Host "📋 Registry index created" -ForegroundColor Green
    }

    $script:VBAFRegistryPath = $Path
    $script:RegistryIndex    = $indexPath
    return $Path
}

function Get-VBAFRegistryIndex {
    if (-not (Test-Path $script:RegistryIndex)) {
        Initialize-VBAFRegistry | Out-Null
    }
    $content = Get-Content $script:RegistryIndex -Raw -Encoding UTF8
    return $content | ConvertFrom-Json
}

function Save-VBAFRegistryIndex {
    param([object]$Index)
    $Index | ConvertTo-Json -Depth 10 | Set-Content $script:RegistryIndex -Encoding UTF8
}

# ============================================================
# VERSION MANAGEMENT
# ============================================================
# TEACHING NOTE: Semantic versioning: MAJOR.MINOR.PATCH
# MAJOR: breaking changes (new architecture)
# MINOR: new features (new layers added)
# PATCH: bug fixes (same architecture, retrained)
#
# Auto-increment strategy:
# First save -> 1.0.0
# Retrain same model -> bump patch (1.0.1)
# New hyperparams -> bump minor (1.1.0)
# New architecture -> bump major (2.0.0)
# ============================================================

function Get-NextVersion {
    param([string]$ModelName, [string]$BumpType = "patch")

    $index = Get-VBAFRegistryIndex
    $modelEntry = $index.Models.$ModelName

    if ($null -eq $modelEntry -or $modelEntry.Versions.Count -eq 0) {
        return "1.0.0"
    }

    # Find latest version
    $versions  = $modelEntry.Versions.PSObject.Properties.Name
    $latest    = $versions | Sort-Object { 
        $parts = $_.Split('.')
        [int]$parts[0] * 10000 + [int]$parts[1] * 100 + [int]$parts[2]
    } | Select-Object -Last 1

    $parts = $latest.Split('.')
    $major = [int]$parts[0]
    $minor = [int]$parts[1]
    $patch = [int]$parts[2]

    switch ($BumpType) {
        "major" { $major++; $minor=0; $patch=0 }
        "minor" { $minor++; $patch=0 }
        "patch" { $patch++ }
    }

    return "$major.$minor.$patch"
}

# ============================================================
# SAVE MODEL
# ============================================================
# TEACHING NOTE: Saving a model means serializing:
# 1. WEIGHTS : the numbers learned during training
# 2. METADATA : what trained this model (hyperparams, dataset)
# 3. METRICS : how well it performed (accuracy, loss)
# 4. VERSION : when and what version this is
#
# We use JSON format - human readable, easy to inspect!
# ============================================================

function Save-VBAFModel {
    param(
        [string]    $ModelName,
        [object]    $Model,
        [string]    $ModelType,           # "LinearRegression", "DecisionTree", "CNN", etc.
        [hashtable] $Metrics      = @{},  # e.g. @{accuracy=0.95; loss=0.12}
        [hashtable] $Params       = @{},  # e.g. @{lr=0.01; epochs=100}
        [string]    $DatasetName  = "",
        [string]    $Description  = "",
        [string]    $BumpType     = "patch"  # "major", "minor", "patch"
    )

    Initialize-VBAFRegistry | Out-Null

    $version   = Get-NextVersion -ModelName $ModelName -BumpType $BumpType
    $timestamp = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
    $modelDir  = Join-Path $script:VBAFRegistryPath "$ModelName\v$version"

    if (-not (Test-Path $modelDir)) {
        New-Item -ItemType Directory -Path $modelDir -Force | Out-Null
    }

    # Serialize weights based on model type
    $weights = Get-ModelWeights -Model $Model -ModelType $ModelType

    # Build model file
    $modelFile = @{
        SchemaVersion = $script:SchemaVersion
        ModelName     = $ModelName
        ModelType     = $ModelType
        Version       = $version
        Timestamp     = $timestamp
        Description   = $Description
        DatasetName   = $DatasetName
        Params        = $Params
        Metrics       = $Metrics
        Weights       = $weights
    }

    $modelPath = Join-Path $modelDir "model.json"
    $modelFile | ConvertTo-Json -Depth 10 | Set-Content $modelPath -Encoding UTF8

    # Update registry index
    $index = Get-VBAFRegistryIndex
    if ($null -eq $index.Models.$ModelName) {
        $index.Models | Add-Member -NotePropertyName $ModelName -NotePropertyValue ([PSCustomObject]@{
            ModelType = $ModelType
            Versions  = [PSCustomObject]@{}
            Latest    = $version
        }) -Force
    }

    $versionEntry = [PSCustomObject]@{
        Timestamp   = $timestamp
        Description = $Description
        Dataset     = $DatasetName
        Metrics     = $Metrics
        Path        = $modelPath
    }
    $index.Models.$ModelName.Versions | Add-Member -NotePropertyName $version -NotePropertyValue $versionEntry -Force
    $index.Models.$ModelName.Latest = $version

    Save-VBAFRegistryIndex -Index $index

    Write-Host ""
    Write-Host ("💾 Model saved: {0} v{1}" -f $ModelName, $version) -ForegroundColor Green
    Write-Host (" Path : {0}" -f $modelPath)     -ForegroundColor DarkGray
    Write-Host (" Type : {0}" -f $ModelType)     -ForegroundColor Cyan
    Write-Host (" Dataset : {0}" -f $DatasetName)   -ForegroundColor Cyan
    if ($Metrics.Count -gt 0) {
        Write-Host " Metrics :" -ForegroundColor Cyan -NoNewline
        foreach ($k in $Metrics.Keys) { Write-Host (" {0}={1}" -f $k, $Metrics[$k]) -NoNewline -ForegroundColor White }
        Write-Host ""
    }
    Write-Host ""
    return $version
}

# ============================================================
# LOAD MODEL
# ============================================================

function Load-VBAFModel {
    param(
        [string] $ModelName,
        [string] $Version = "latest"
    )

    Initialize-VBAFRegistry | Out-Null
    $index = Get-VBAFRegistryIndex

    if ($null -eq $index.Models.$ModelName) {
        Write-Host "❌ Model not found: $ModelName" -ForegroundColor Red
        Write-Host " Use Get-VBAFModelList to see available models" -ForegroundColor Yellow
        return $null
    }

    $resolvedVersion = if ($Version -eq "latest") {
        $index.Models.$ModelName.Latest
    } else { $Version }

    $modelPath = Join-Path $script:VBAFRegistryPath "$ModelName\v$resolvedVersion\model.json"
    if (-not (Test-Path $modelPath)) {
        Write-Host ("❌ Version {0} not found for {1}" -f $resolvedVersion, $ModelName) -ForegroundColor Red
        return $null
    }

    $modelFile = Get-Content $modelPath -Raw -Encoding UTF8 | ConvertFrom-Json

    # Schema version check - backwards compatibility
    if ($modelFile.SchemaVersion -ne $script:SchemaVersion) {
        Write-Host ("⚠️ Schema version mismatch: file={0} current={1}" -f $modelFile.SchemaVersion, $script:SchemaVersion) -ForegroundColor Yellow
        Write-Host " Attempting load anyway..." -ForegroundColor Yellow
    }

    Write-Host ""
    Write-Host ("📂 Model loaded: {0} v{1}" -f $ModelName, $resolvedVersion) -ForegroundColor Green
    Write-Host (" Type : {0}" -f $modelFile.ModelType)    -ForegroundColor Cyan
    Write-Host (" Saved : {0}" -f $modelFile.Timestamp)    -ForegroundColor Cyan
    Write-Host (" Dataset : {0}" -f $modelFile.DatasetName)  -ForegroundColor Cyan
    if ($null -ne $modelFile.Metrics) {
        Write-Host " Metrics :" -ForegroundColor Cyan -NoNewline
        $modelFile.Metrics.PSObject.Properties | ForEach-Object {
            Write-Host (" {0}={1}" -f $_.Name, $_.Value) -NoNewline -ForegroundColor White
        }
        Write-Host ""
    }
    Write-Host ""

    return $modelFile
}

# ============================================================
# WEIGHT EXTRACTION
# ============================================================
# TEACHING NOTE: Different model types store weights differently.
# We need a unified way to extract and restore them.
# ============================================================

function Get-ModelWeights {
    param([object]$Model, [string]$ModelType)

    switch ($ModelType) {
        "LinearRegression" {
            return @{ Weights=$Model.Weights; Bias=$Model.Bias }
        }
        "RidgeRegression" {
            return @{ Weights=$Model.Weights; Bias=$Model.Bias; Lambda=$Model.Lambda }
        }
        "LogisticRegression" {
            return @{ Weights=$Model.Weights; Bias=$Model.Bias }
        }
        "DecisionTree" {
            return @{ Serialized="DecisionTree weights not serializable - retrain required" }
        }
        "RandomForest" {
            return @{ Serialized="RandomForest weights not serializable - retrain required" }
        }
        "GaussianNaiveBayes" {
            return @{
                ClassPriors  = $Model.ClassPriors
                FeatureMeans = $Model.FeatureMeans
                FeatureVars  = $Model.FeatureVars
                Classes      = $Model.Classes
            }
        }
        "KMeans" {
            return @{ Centroids=$Model.Centroids; K=$Model.K }
        }
        "StandardScaler" {
            return @{ Mean=$Model.Mean; Std=$Model.Std }
        }
        "MinMaxScaler" {
            return @{ Min=$Model.Min; Max=$Model.Max }
        }
        "CNN" {
            # Use existing CNN weight serialization
            $layers = @()
            foreach ($layer in $Model.Layers) {
                $typeName = $layer.GetType().Name
                $layerData = @{ Type=$typeName }
                if ($typeName -eq "Conv2D") {
                    $layerData.Weights = $layer.Weights.Data
                    $layerData.Biases  = $layer.Biases
                }
                if ($typeName -eq "DenseLayer") {
                    $layerData.Weights = $layer.Weights
                    $layerData.Biases  = $layer.Biases
                }
                $layers += $layerData
            }
            return @{ Layers=$layers; InputH=$Model.InputH; InputW=$Model.InputW; InputC=$Model.InputC }
        }
        "Generic" {
            # Fallback - serialize whatever properties the model has
            $weights = @{}
            $Model.PSObject.Properties | ForEach-Object {
                $weights[$_.Name] = $_.Value
            }
            return $weights
        }
        default {
            return @{ Note="Unknown model type - manual weight extraction required" }
        }
    }
}

# ============================================================
# REGISTRY OPERATIONS
# ============================================================

function Get-VBAFModelList {
    param([string]$ModelName = "")

    Initialize-VBAFRegistry | Out-Null
    $index = Get-VBAFRegistryIndex

    Write-Host ""
    Write-Host "╔══════════════════════════════════════════════════════════╗" -ForegroundColor Cyan
    Write-Host "║ VBAF Model Registry ║" -ForegroundColor Cyan
    Write-Host ("║ Path: {0,-51}║" -f ($script:VBAFRegistryPath -replace "^.{0,3}", "...")) -ForegroundColor DarkGray
    Write-Host "╠══════════════════════════════════════════════════════════╣" -ForegroundColor Cyan

    $models = $index.Models.PSObject.Properties
    if ($null -eq $models -or @($models).Count -eq 0) {
        Write-Host "║ No models saved yet ║" -ForegroundColor Yellow
    } else {
        foreach ($modelProp in $models) {
            $name  = $modelProp.Name
            if ($ModelName -ne "" -and $name -ne $ModelName) { continue }
            $entry = $modelProp.Value
            Write-Host ("║ 📦 {0,-52}║" -f $name) -ForegroundColor White
            Write-Host ("║ Type : {0,-44}║" -f $entry.ModelType) -ForegroundColor DarkGray
            Write-Host ("║ Latest : v{0,-43}║" -f $entry.Latest) -ForegroundColor Green

            $versions = $entry.Versions.PSObject.Properties
            foreach ($vProp in $versions) {
                $vName = $vProp.Name
                $vData = $vProp.Value
                $metricsStr = ""
                if ($null -ne $vData.Metrics) {
                    $vData.Metrics.PSObject.Properties | ForEach-Object {
                        $metricsStr += "{0}={1} " -f $_.Name, $_.Value
                    }
                }
                Write-Host ("║ v{0,-8} {1,-41}║" -f $vName, $metricsStr.Trim()) -ForegroundColor DarkGray
            }
            Write-Host "║ ║" -ForegroundColor Cyan
        }
    }
    Write-Host "╚══════════════════════════════════════════════════════════╝" -ForegroundColor Cyan
    Write-Host ""
}

function Remove-VBAFModel {
    param([string]$ModelName, [string]$Version = "")

    $index = Get-VBAFRegistryIndex
    if ($null -eq $index.Models.$ModelName) {
        Write-Host "❌ Model not found: $ModelName" -ForegroundColor Red
        return
    }

    if ($Version -eq "") {
        # Remove all versions
        $modelDir = Join-Path $script:VBAFRegistryPath $ModelName
        if (Test-Path $modelDir) { Remove-Item $modelDir -Recurse -Force }
        $index.Models.PSObject.Properties.Remove($ModelName)
        Save-VBAFRegistryIndex -Index $index
        Write-Host "🗑️ Removed model: $ModelName (all versions)" -ForegroundColor Yellow
    } else {
        # Remove specific version
        $versionDir = Join-Path $script:VBAFRegistryPath "$ModelName\v$Version"
        if (Test-Path $versionDir) { Remove-Item $versionDir -Recurse -Force }
        $index.Models.$ModelName.Versions.PSObject.Properties.Remove($Version)
        # Update latest if needed
        $remaining = @($index.Models.$ModelName.Versions.PSObject.Properties.Name)
        if ($remaining.Length -gt 0) {
            $index.Models.$ModelName.Latest = $remaining | Sort-Object | Select-Object -Last 1
        } else {
            $index.Models.PSObject.Properties.Remove($ModelName)
        }
        Save-VBAFRegistryIndex -Index $index
        Write-Host ("🗑️ Removed: {0} v{1}" -f $ModelName, $Version) -ForegroundColor Yellow
    }
}

function Compare-VBAFModels {
    param([string]$ModelName)

    Initialize-VBAFRegistry | Out-Null
    $index = Get-VBAFRegistryIndex
    if ($null -eq $index.Models.$ModelName) {
        Write-Host "❌ Model not found: $ModelName" -ForegroundColor Red
        return
    }

    Write-Host ""
    Write-Host ("📊 Version Comparison: {0}" -f $ModelName) -ForegroundColor Green
    Write-Host (" {0,-10} {1,-20} {2,-30}" -f "Version", "Saved", "Metrics") -ForegroundColor Yellow
    Write-Host (" {0}" -f ("-" * 62)) -ForegroundColor DarkGray

    $entry    = $index.Models.$ModelName
    if ($null -eq $entry.Versions) { Write-Host "No versions found" -ForegroundColor Yellow; return }
    $versions = $entry.Versions.PSObject.Properties | Sort-Object Name

    foreach ($vProp in $versions) {
        $vName = $vProp.Name
        $vData = $vProp.Value
        $metricsStr = ""
        if ($null -ne $vData.Metrics) {
            $vData.Metrics.PSObject.Properties | ForEach-Object {
                $metricsStr += "{0}={1} " -f $_.Name, [Math]::Round([double]$_.Value, 4)
            }
        }
        $isLatest = if ($vName -eq $entry.Latest) { " ← latest" } else { "" }
        $color    = if ($vName -eq $entry.Latest) { "Green" } else { "White" }
        Write-Host (" {0,-10} {1,-20} {2}{3}" -f "v$vName", $vData.Timestamp, $metricsStr.Trim(), $isLatest) -ForegroundColor $color
    }
    Write-Host ""
}

function Set-VBAFRegistryPath {
    param([string]$Path)
    $script:VBAFRegistryPath = $Path
    $script:RegistryIndex    = Join-Path $Path "registry.json"
    Initialize-VBAFRegistry -Path $Path | Out-Null
    Write-Host "📁 Registry path set: $Path" -ForegroundColor Green
}

# ============================================================
# METADATA HELPERS
# ============================================================

function New-VBAFModelMetadata {
    param(
        [string]    $ModelName,
        [string]    $ModelType,
        [string]    $Description  = "",
        [string]    $DatasetName  = "",
        [hashtable] $Params       = @{},
        [hashtable] $Metrics      = @{}
    )
    return @{
        ModelName   = $ModelName
        ModelType   = $ModelType
        Description = $Description
        DatasetName = $DatasetName
        Params      = $Params
        Metrics     = $Metrics
    }
}

function Get-VBAFModelInfo {
    param([string]$ModelName, [string]$Version = "latest")
    $modelFile = Load-VBAFModel -ModelName $ModelName -Version $Version
    if ($null -eq $modelFile) { return }

    Write-Host "╔══════════════════════════════════════╗" -ForegroundColor Cyan
    Write-Host ("║ Model: {0,-29}║" -f $ModelName) -ForegroundColor White
    Write-Host ("║ Version: {0,-27}║" -f $modelFile.Version) -ForegroundColor White
    Write-Host ("║ Type : {0,-27}║" -f $modelFile.ModelType) -ForegroundColor White
    Write-Host ("║ Saved : {0,-27}║" -f $modelFile.Timestamp) -ForegroundColor White
    Write-Host ("║ Dataset: {0,-27}║" -f $modelFile.DatasetName) -ForegroundColor White
    if ($modelFile.Description -ne "") {
        Write-Host ("║ Desc : {0,-27}║" -f $modelFile.Description) -ForegroundColor DarkGray
    }
    Write-Host "╠══════════════════════════════════════╣" -ForegroundColor Cyan
    Write-Host "║ Params:" -ForegroundColor Yellow
    if ($null -ne $modelFile.Params) {
        $modelFile.Params.PSObject.Properties | ForEach-Object {
            Write-Host ("║ {0,-34}║" -f ("{0} = {1}" -f $_.Name, $_.Value)) -ForegroundColor White
        }
    }
    Write-Host "║ Metrics:" -ForegroundColor Yellow
    if ($null -ne $modelFile.Metrics) {
        $modelFile.Metrics.PSObject.Properties | ForEach-Object {
            Write-Host ("║ {0,-34}║" -f ("{0} = {1}" -f $_.Name, $_.Value)) -ForegroundColor Green
        }
    }
    Write-Host "╚══════════════════════════════════════╝" -ForegroundColor Cyan
}

# ============================================================
# TEST
# 1. Run VBAF.LoadAll.ps1
#
# --- Initialize registry ---
# 2. Initialize-VBAFRegistry
# Get-VBAFModelList
#
# --- Save a regression model ---
# 3. $data = Get-VBAFDataset -Name "HousePrice"
# $scaler = [StandardScaler]::new()
# $Xs = $scaler.FitTransform($data.X)
# $model = [LinearRegression]::new()
# $model.Fit($Xs, $data.y)
# $metrics = @{ R2=0.998; RMSE=2.1 }
# $params = @{ LearningRate=0.01; Epochs=1000 }
# Save-VBAFModel -ModelName "HousePricePredictor" -Model $model `
# -ModelType "LinearRegression" -Metrics $metrics -Params $params `
# -DatasetName "HousePrice" -Description "Baseline linear model"
#
# --- List and inspect ---
# 4. Get-VBAFModelList
# Get-VBAFModelInfo -ModelName "HousePricePredictor"
#
# --- Save v2 with better metrics ---
# 5. $model2 = [RidgeRegression]::new(0.1)
# $model2.Fit($Xs, $data.y)
# $metrics2 = @{ R2=0.999; RMSE=1.8 }
# Save-VBAFModel -ModelName "HousePricePredictor" -Model $model2 `
# -ModelType "RidgeRegression" -Metrics $metrics2 `
# -DatasetName "HousePrice" -Description "Ridge regularization" -BumpType "minor"
#
# --- Compare versions ---
# 6. Compare-VBAFModels -ModelName "HousePricePredictor"
#
# --- Load specific version ---
# 7. $loaded = Load-VBAFModel -ModelName "HousePricePredictor" -Version "1.0.0"
# $loaded.Metrics
# $loaded.Weights
#
# --- Save a KMeans model ---
# 8. $data2 = Get-VBAFClusterDataset -Name "Blobs"
# $km = [KMeans]::new(3)
# $km.Fit($data2.X)
# Save-VBAFModel -ModelName "BlobClusterer" -Model $km -ModelType "KMeans" `
# -Metrics @{ Silhouette=0.92 } -DatasetName "Blobs"
#
# --- Full registry view ---
# 9. Get-VBAFModelList
# ============================================================
Write-Host "📦 VBAF.ML.ModelRegistry.ps1 loaded [v2.1.0 🏭]" -ForegroundColor Green
Write-Host " Functions : Initialize-VBAFRegistry"            -ForegroundColor Cyan
Write-Host " Save-VBAFModel"                      -ForegroundColor Cyan
Write-Host " Load-VBAFModel"                      -ForegroundColor Cyan
Write-Host " Get-VBAFModelList"                   -ForegroundColor Cyan
Write-Host " Get-VBAFModelInfo"                   -ForegroundColor Cyan
Write-Host " Compare-VBAFModels"                  -ForegroundColor Cyan
Write-Host " Remove-VBAFModel"                    -ForegroundColor Cyan
Write-Host " Set-VBAFRegistryPath"                -ForegroundColor Cyan
Write-Host " New-VBAFModelMetadata"               -ForegroundColor Cyan
Write-Host " Get-NextVersion"                     -ForegroundColor Cyan
Write-Host ""
Write-Host " Registry : $($script:VBAFRegistryPath)"        -ForegroundColor DarkGray
Write-Host ""
Write-Host " Quick start:" -ForegroundColor Yellow
Write-Host ' Initialize-VBAFRegistry'                                        -ForegroundColor White
Write-Host ' Save-VBAFModel -ModelName "MyModel" -Model $model -ModelType "LinearRegression" -Metrics @{R2=0.99}' -ForegroundColor White
Write-Host ' Get-VBAFModelList'                                              -ForegroundColor White
Write-Host ' Compare-VBAFModels -ModelName "MyModel"'                       -ForegroundColor White
Write-Host ""