#!/usr/bin/env pwsh <# .SYNOPSIS Freeze an approved model/dataset VersionSet for Phase 1 shadow run. .DESCRIPTION Parameterized tool to INSERT approved model_id + dataset_id into: - governance.model_version_registry (FROZEN status) - evaluation.dataset_manifest (FROZEN status) NO default values; all parameters REQUIRED. Fails immediately if any parameter is missing. .PARAMETER ModelId UUID of the approved model (e.g., "00000000-0000-0000-0000-000000000001") Required. No default. .PARAMETER DatasetId UUID of the approved dataset (e.g., "00000000-0000-0000-0000-000000000002") Required. No default. .PARAMETER ApprovedBy Email/ID of the approver (e.g., "kjh2064@gmail.com") Required. No default. .PARAMETER ConfigVersion Configuration version string (e.g., "v1.0.0") Required. No default. .PARAMETER CodeSha Git commit SHA (e.g., "acaa731b3f") Required. No default. .PARAMETER ConnectionString PostgreSQL connection string. Default: $env:KARTSELL_POSTGRES .EXAMPLE # Freeze a versionset (all parameters required) .\freeze-versionset.ps1 ` -ModelId "00000000-0000-0000-0000-000000000001" ` -DatasetId "00000000-0000-0000-0000-000000000002" ` -ApprovedBy "kjh2064@gmail.com" ` -ConfigVersion "v1.0.0" ` -CodeSha "acaa731b3f" .EXAMPLE # Will fail: missing -ConfigVersion .\freeze-versionset.ps1 ` -ModelId "00000000-0000-0000-0000-000000000001" ` -DatasetId "00000000-0000-0000-0000-000000000002" ` -ApprovedBy "kjh2064@gmail.com" ` -CodeSha "acaa731b3f" # Error: Cannot bind argument to parameter 'ConfigVersion' because it is an empty string. #> [CmdletBinding()] param( [Parameter(Mandatory, HelpMessage = "Model UUID (e.g., 00000000-0000-0000-0000-000000000001)")] [ValidateScript({ $_ -match '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' })] [string]$ModelId, [Parameter(Mandatory, HelpMessage = "Dataset UUID")] [ValidateScript({ $_ -match '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' })] [string]$DatasetId, [Parameter(Mandatory, HelpMessage = "Approver email/ID (e.g., kjh2064@gmail.com)")] [ValidateScript({ $_ -match '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' })] [string]$ApprovedBy, [Parameter(Mandatory, HelpMessage = "Config version (e.g., v1.0.0)")] [ValidateScript({ $_ -match '^v[0-9]+\.[0-9]+\.[0-9]+' })] [string]$ConfigVersion, [Parameter(Mandatory, HelpMessage = "Git commit SHA (at least 10 chars)")] [ValidateScript({ $_.Length -ge 10 })] [string]$CodeSha, [string]$ConnectionString = $env:KARTSELL_POSTGRES ) $ErrorActionPreference = 'Stop' Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor Cyan Write-Host "Phase 1: Freeze VersionSet" -ForegroundColor Cyan Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor Cyan # Validate connection string if (-not $ConnectionString) { Write-Error "ConnectionString not provided and `$env:KARTSELL_POSTGRES not set. Aborting." exit 1 } Write-Host "`n[1/3] PRE-FLIGHT CHECK" Write-Host " Model ID: $ModelId" Write-Host " Dataset ID: $DatasetId" Write-Host " Approved By: $ApprovedBy" Write-Host " Config Version: $ConfigVersion" Write-Host " Code SHA: $CodeSha" Write-Host " Connection: $(($ConnectionString -split 'Password=')[0])***" # Verify 0032 migration is deployed Write-Host "`n[2/3] VERIFY Migration 0032 deployed..." try { $conn = New-Object System.Data.NpgsqlClient.NpgsqlConnection($ConnectionString) $conn.Open() $cmd = $conn.CreateCommand() $cmd.CommandText = @" SELECT schema_version FROM schema_version_history WHERE script_name = '0032_shadow_run_queued_status_contract.sql' LIMIT 1 "@ $result = $cmd.ExecuteScalar() if ($null -eq $result) { throw "Migration 0032 NOT FOUND. Run DbMigrator first." } Write-Host " ✅ Migration 0032 deployed (schema_version: $result)" $conn.Close() } catch { Write-Error " ❌ Pre-flight failed: $_`n`nCorrective: Run DbMigrator to deploy 0032_*.sql before freezing." exit 1 } # Insert into governance.model_version_registry Write-Host "`n[3/3] FREEZE VersionSet..." try { $conn = New-Object System.Data.NpgsqlClient.NpgsqlConnection($ConnectionString) $conn.Open() $correlationId = [System.Guid]::NewGuid() $now = [System.DateTime]::UtcNow $cmd = $conn.CreateCommand() $cmd.CommandText = @" INSERT INTO governance.model_version_registry ( id, model_id, dataset_id, status, approved_by, config_version, code_sha, effective_at, published_at, revision, correlation_id ) VALUES ( @id, @model_id, @dataset_id, 'FROZEN', @approved_by, @config_version, @code_sha, @effective_at, @published_at, 1, @correlation_id ) ON CONFLICT (model_id, dataset_id) DO UPDATE SET status = 'FROZEN', approved_by = EXCLUDED.approved_by, config_version = EXCLUDED.config_version, code_sha = EXCLUDED.code_sha, effective_at = EXCLUDED.effective_at, revision = governance.model_version_registry.revision + 1, published_at = EXCLUDED.published_at RETURNING id, model_id, dataset_id, status, effective_at "@ $cmd.Parameters.AddWithValue("@id", [System.Guid]::NewGuid()) | Out-Null $cmd.Parameters.AddWithValue("@model_id", [System.Guid]$ModelId) | Out-Null $cmd.Parameters.AddWithValue("@dataset_id", [System.Guid]$DatasetId) | Out-Null $cmd.Parameters.AddWithValue("@approved_by", $ApprovedBy) | Out-Null $cmd.Parameters.AddWithValue("@config_version", $ConfigVersion) | Out-Null $cmd.Parameters.AddWithValue("@code_sha", $CodeSha) | Out-Null $cmd.Parameters.AddWithValue("@effective_at", $now) | Out-Null $cmd.Parameters.AddWithValue("@published_at", $now) | Out-Null $cmd.Parameters.AddWithValue("@correlation_id", $correlationId) | Out-Null $reader = $cmd.ExecuteReader() if ($reader.Read()) { $insertedId = $reader['id'] $insertedModelId = $reader['model_id'] $insertedDatasetId = $reader['dataset_id'] $insertedStatus = $reader['status'] Write-Host " ✅ Inserted governance.model_version_registry:" Write-Host " - ID: $insertedId" Write-Host " - Model: $insertedModelId" Write-Host " - Dataset: $insertedDatasetId" Write-Host " - Status: $insertedStatus" } $reader.Close() # Update evaluation.dataset_manifest $cmd2 = $conn.CreateCommand() $cmd2.CommandText = @" INSERT INTO evaluation.dataset_manifest ( id, dataset_id, model_id, status, freeze_reason, published_at, revision, correlation_id ) VALUES ( @id, @dataset_id, @model_id, 'FROZEN', 'Phase 1 VersionSet freeze', @published_at, 1, @correlation_id ) ON CONFLICT (dataset_id, model_id) DO UPDATE SET status = 'FROZEN', freeze_reason = 'Phase 1 VersionSet freeze', revision = evaluation.dataset_manifest.revision + 1, published_at = EXCLUDED.published_at RETURNING id, dataset_id, model_id, status "@ $cmd2.Parameters.AddWithValue("@id", [System.Guid]::NewGuid()) | Out-Null $cmd2.Parameters.AddWithValue("@dataset_id", [System.Guid]$DatasetId) | Out-Null $cmd2.Parameters.AddWithValue("@model_id", [System.Guid]$ModelId) | Out-Null $cmd2.Parameters.AddWithValue("@published_at", $now) | Out-Null $cmd2.Parameters.AddWithValue("@correlation_id", $correlationId) | Out-Null $reader2 = $cmd2.ExecuteReader() if ($reader2.Read()) { $mId = $reader2['id'] $mDatasetId = $reader2['dataset_id'] $mModelId = $reader2['model_id'] $mStatus = $reader2['status'] Write-Host " ✅ Inserted evaluation.dataset_manifest:" Write-Host " - ID: $mId" Write-Host " - Dataset: $mDatasetId" Write-Host " - Model: $mModelId" Write-Host " - Status: $mStatus" } $reader2.Close() $conn.Close() Write-Host "`n✅ VersionSet FROZEN successfully" Write-Host " Correlation ID: $correlationId" Write-Host " Next: Run generate-shadow-run-identifiers.ps1 to create RunId/JobId" } catch { Write-Error " ❌ Failed to freeze VersionSet: $_" exit 1 } Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor Cyan