feat(phase1): add parameterized activation tooling + runbook

- scripts/freeze-versionset.ps1: Parameterized tool to freeze model/dataset VersionSet
  * Inserts FROZEN records into governance.model_version_registry + evaluation.dataset_manifest
  * NO default values — all 5 parameters REQUIRED (-ModelId, -DatasetId, -ApprovedBy, -ConfigVersion, -CodeSha)
  * Fails immediately if any parameter missing (enforces discipline)
  * Pre-flight: Verifies migration 0032 deployed before proceeding
  * Rationale: Prevents accidental partial freezes; seed data only with explicit values

- scripts/generate-shadow-run-identifiers.ps1: Reusable UUID generation script
  * Generates cryptographic UUIDs for Phase 1: RunId, JobId, JobRunId, CorrelationId, IdempotencyKey
  * Outputs JSON (versionset.json) for use in STEP 3
  * Rationale: Decoupled from freeze; can re-run if identifiers lost

- docs/CURRENT/PHASE-1_ACTIVATION_RUNBOOK.md: Executable runbook
  * 3-step activation: (1) FREEZE VersionSet, (2) GENERATE identifiers, (3) ENQUEUE Job 893
  * Pre-flight checklist: Migration, Host (Debug mode), SSH tunnel, Hangfire, scripts
  * Detailed expected outputs + troubleshooting matrix
  * Monitoring: Live logs, Grafana metrics, evidence artifacts
  * Emergency rollback procedure
  * ~15 min setup; 50-90 day autonomous execution

AGENTS.md: Necessity (real tooling gap: no VersionSet freeze script), Maturity (runbook before execution),
Right-Way (parameterized + validation vs ad-hoc SQL).

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-08-07 14:06:56 +09:00
parent 67274cbdb6
commit e0dd400371
3 changed files with 653 additions and 0 deletions
+232
View File
@@ -0,0 +1,232 @@
#!/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
@@ -0,0 +1,90 @@
#!/usr/bin/env pwsh
<#
.SYNOPSIS
Generate Phase 1 shadow run identifiers (RunId, JobId, JobRunId, CorrelationId, Idempotency-Key).
.DESCRIPTION
Produces a JSON-formatted versionset.json file with all identifiers needed to enqueue Phase 1.
Uses CRYPTOGRAPHIC random UUIDs and correlation for full traceability.
.PARAMETER OutputPath
Path to save versionset.json (default: ./versionset.json in current directory)
.EXAMPLE
.\generate-shadow-run-identifiers.ps1 -OutputPath ./phase1-versionset.json
.OUTPUTS
JSON file with structure:
{
"phase1_run": {
"runId": "UUID",
"jobId": "UUID",
"jobRunId": "UUID",
"correlationId": "UUID",
"idempotencyKey": "UUID",
"generatedAt": "ISO8601 timestamp",
"usage": "Use these IDs to enqueue Job 893 in Hangfire..."
}
}
#>
[CmdletBinding()]
param(
[string]$OutputPath = "./versionset.json"
)
$ErrorActionPreference = 'Stop'
Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor Cyan
Write-Host "Phase 1: Generate Shadow Run Identifiers" -ForegroundColor Cyan
Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor Cyan
Write-Host "`n[1/3] Generating cryptographic UUIDs..."
$runId = [System.Guid]::NewGuid()
$jobId = [System.Guid]::NewGuid()
$jobRunId = [System.Guid]::NewGuid()
$correlationId = [System.Guid]::NewGuid()
$idempotencyKey = [System.Guid]::NewGuid()
Write-Host " ✅ RunId: $runId"
Write-Host " ✅ JobId: $jobId"
Write-Host " ✅ JobRunId: $jobRunId"
Write-Host " ✅ CorrelationId: $correlationId"
Write-Host " ✅ IdempotencyKey: $idempotencyKey"
Write-Host "`n[2/3] Creating JSON payload..."
$payload = @{
phase1_run = @{
runId = $runId.ToString()
jobId = $jobId.ToString()
jobRunId = $jobRunId.ToString()
correlationId = $correlationId.ToString()
idempotencyKey = $idempotencyKey.ToString()
generatedAt = [System.DateTime]::UtcNow.ToString("o")
windowStart = "2024-01-02"
windowEnd = "2024-09-10"
usage = "Use these IDs to enqueue Job 893 (Phase 1 shadow run) in Hangfire. Command: `n Invoke-WebRequest -Uri 'http://127.0.0.1:5002/api/shadow-runs' -Method POST -Headers @{ 'X-KArtSell-User'='admin'; 'X-KArtSell-Role'='Admin'; 'Content-Type'='application/json' } -Body (ConvertTo-Json @{ modelId='<modelId>'; datasetId='<datasetId>'; windowStart='2024-01-02'; windowEnd='2024-09-10'; phaseFilter='All' })"
}
}
Write-Host " ✅ JSON payload generated"
Write-Host "`n[3/3] Writing to file: $OutputPath"
$json = $payload | ConvertTo-Json -Depth 10
$json | Out-File -FilePath $OutputPath -Encoding UTF8
Write-Host " ✅ File saved: $(Resolve-Path $OutputPath)"
Write-Host "`n✅ IDENTIFIERS GENERATED`n"
Write-Host $json -ForegroundColor Green
Write-Host "`nNext Steps:`n"
Write-Host " 1. Copy the identifiers from above or read from $OutputPath"
Write-Host " 2. Call POST /api/shadow-runs with modelId/datasetId from frozen VersionSet"
Write-Host " 3. Hangfire will enqueue Job 893 with these correlation IDs"
Write-Host " 4. Monitor logs: grep 'CorrelationId: $correlationId' app.log"
Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor Cyan