Public/Invoke-SchemaMigration.ps1

# Copyright (c) 2026 Jeffrey Snover. All rights reserved.
# Licensed under the MIT License. See LICENSE file in the project root.

function Invoke-SchemaMigration {
    <#
    .SYNOPSIS
        Migrates taxonomy data from one schema version to another.
    .DESCRIPTION
        Detects the current data schema version by inspecting taxonomy nodes
        and the policy registry, then applies the necessary migration steps.

        Schema detection:
        - If nodes have policy_actions with policy_id fields -> 1.1.0+
        - If nodes have policy_actions without policy_id -> 1.0.0
        - If nodes have no policy_actions at all -> 0.x (pre-policy)

        Migration steps:
        - 1.0.0 -> 1.1.0: Calls Update-PolicyRegistry -Fix to assign IDs to
          all unregistered policy_actions entries.
        - Any version: Regenerates embeddings via Update-TaxEmbeddings.
        - Bumps TAXONOMY_VERSION file to reflect the migration.
    .PARAMETER TargetVersion
        The schema version to migrate to. Defaults to '1.1.0'.
    .PARAMETER DryRun
        Show what would be done without making changes.
    .PARAMETER PassThru
        Return a migration summary object.
    .EXAMPLE
        Invoke-SchemaMigration
    .EXAMPLE
        Invoke-SchemaMigration -DryRun
    .EXAMPLE
        Invoke-SchemaMigration -PassThru
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [string]$TargetVersion = '1.1.0',

        [switch]$DryRun,

        [switch]$PassThru
    )

    Set-StrictMode -Version Latest
    $ErrorActionPreference = 'Stop'

    # -- Paths -----------------------------------------------------------------
    $TaxDir      = Get-TaxonomyDir
    $VersionFile = Get-VersionFile

    if (-not (Test-Path $TaxDir)) {
        Write-Fail "Taxonomy directory not found: $TaxDir"
        throw 'Taxonomy directory not found'
    }

    # -- Detect current schema version -----------------------------------------
    Write-Step 'Detecting current data schema'

    $PovFiles = @('accelerationist', 'safetyist', 'skeptic', 'cross-cutting')
    $TotalNodes           = 0
    $NodesWithPolicyId    = 0
    $NodesWithActions     = 0
    $NodesWithoutPolicyId = 0

    foreach ($PovKey in $PovFiles) {
        $FilePath = Join-Path $TaxDir "$PovKey.json"
        if (-not (Test-Path $FilePath)) { continue }
        $FileData = Get-Content -Raw -Path $FilePath | ConvertFrom-Json

        foreach ($Node in $FileData.nodes) {
            $TotalNodes++
            if (-not $Node.PSObject.Properties['graph_attributes'] -or $null -eq $Node.graph_attributes) { continue }
            if (-not $Node.graph_attributes.PSObject.Properties['policy_actions']) { continue }

            foreach ($PA in $Node.graph_attributes.policy_actions) {
                $NodesWithActions++
                if ($PA.PSObject.Properties['policy_id'] -and $null -ne $PA.policy_id) {
                    $NodesWithPolicyId++
                }
                else {
                    $NodesWithoutPolicyId++
                }
            }
        }
    }

    # Determine detected version
    $DetectedVersion = if ($NodesWithActions -eq 0) {
        '0.x'
    }
    elseif ($NodesWithoutPolicyId -gt 0) {
        '1.0.0'
    }
    else {
        '1.1.0'
    }

    Write-OK "Total nodes scanned : $TotalNodes"
    Write-OK "Policy actions found : $NodesWithActions"
    Write-OK " With policy_id : $NodesWithPolicyId"
    Write-OK " Without policy_id : $NodesWithoutPolicyId"
    Write-OK "Detected schema : $DetectedVersion"
    Write-OK "Target schema : $TargetVersion"

    # -- Read current taxonomy version -----------------------------------------
    $CurrentTaxVersion = if (Test-Path $VersionFile) {
        (Get-Content -Path $VersionFile -Raw).Trim()
    }
    else {
        '0.0.0'
    }
    Write-OK "Current TAXONOMY_VERSION: $CurrentTaxVersion"

    # -- DryRun ----------------------------------------------------------------
    if ($DryRun) {
        Write-Host "`n$('=' * 60)" -ForegroundColor DarkGray
        Write-Host ' DRY RUN -- Migration Plan' -ForegroundColor Yellow
        Write-Host "$('=' * 60)" -ForegroundColor DarkGray

        if ($DetectedVersion -eq '1.0.0') {
            Write-Host ' [1] 1.0.0 -> 1.1.0: Update-PolicyRegistry -Fix' -ForegroundColor Cyan
            Write-Host " Assigns policy_id to $NodesWithoutPolicyId unregistered action(s)" -ForegroundColor White
        }
        elseif ($DetectedVersion -eq '0.x') {
            Write-Host ' [1] 0.x -> 1.1.0: No policy actions to migrate' -ForegroundColor Gray
        }
        else {
            Write-Host ' [1] Schema already at 1.1.0 -- no ID assignment needed' -ForegroundColor Green
        }

        Write-Host ' [2] Regenerate embeddings via Update-TaxEmbeddings' -ForegroundColor Cyan
        Write-Host " [3] Bump TAXONOMY_VERSION: $CurrentTaxVersion -> (incremented)" -ForegroundColor Cyan
        Write-Host "$('=' * 60)" -ForegroundColor DarkGray
        Write-Host ' No changes made.' -ForegroundColor Yellow
        Write-Host ''

        if ($PassThru) {
            return [PSCustomObject]@{
                DetectedSchema = $DetectedVersion
                TargetSchema   = $TargetVersion
                DryRun         = $true
                MigratedIds    = 0
                EmbeddingsRegen = $false
                VersionBumped  = $false
            }
        }
        return
    }

    # -- Step 1: Schema-specific migration -------------------------------------
    $MigratedIds = 0

    if ($DetectedVersion -eq '1.0.0') {
        Write-Step '1.0.0 -> 1.1.0: Assigning policy IDs to unregistered actions'

        if ($PSCmdlet.ShouldProcess('policy_actions.json', 'Rebuild registry and assign IDs')) {
            $RegistryResult = Update-PolicyRegistry -Fix -Confirm:$false -PassThru
            $MigratedIds = $RegistryResult.Unregistered
            Write-OK "Registry rebuilt: $($RegistryResult.TotalPolicies) policies, $MigratedIds newly assigned"
        }
    }
    elseif ($DetectedVersion -eq '0.x') {
        Write-Info 'No policy actions found -- skipping ID assignment'
    }
    else {
        Write-Info 'Schema already at 1.1.0 -- running registry rebuild for consistency'
        if ($PSCmdlet.ShouldProcess('policy_actions.json', 'Rebuild registry')) {
            Update-PolicyRegistry -Fix -Confirm:$false | Out-Null
            Write-OK 'Registry consistency check complete'
        }
    }

    # -- Step 2: Regenerate embeddings -----------------------------------------
    Write-Step 'Regenerating taxonomy embeddings'

    $EmbeddingsRegen = $false
    if ($PSCmdlet.ShouldProcess('embeddings.json', 'Regenerate taxonomy embeddings')) {
        try {
            Update-TaxEmbeddings
            $EmbeddingsRegen = $true
            Write-OK 'Embeddings regenerated'
        }
        catch {
            Write-Warn "Embeddings regeneration failed: $_ -- you can re-run Update-TaxEmbeddings manually"
        }
    }

    # -- Step 3: Bump TAXONOMY_VERSION -----------------------------------------
    Write-Step 'Bumping TAXONOMY_VERSION'

    $VersionBumped = $false
    $Parts = $CurrentTaxVersion -split '\.'
    if ($Parts.Count -ge 3) {
        $NewPatch = ([int]$Parts[2]) + 1
        $NewVersion = "$($Parts[0]).$($Parts[1]).$NewPatch"
    }
    else {
        $NewVersion = '1.1.0'
    }

    if ($PSCmdlet.ShouldProcess($VersionFile, "Bump version $CurrentTaxVersion -> $NewVersion")) {
        Set-Content -Path $VersionFile -Value $NewVersion -Encoding UTF8 -NoNewline
        $VersionBumped = $true
        Write-OK "TAXONOMY_VERSION bumped: $CurrentTaxVersion -> $NewVersion"
    }

    # -- Summary ---------------------------------------------------------------
    Write-Host ''
    Write-Host '=== Schema Migration Complete ===' -ForegroundColor Cyan
    Write-Host " Detected schema : $DetectedVersion" -ForegroundColor White
    Write-Host " Target schema : $TargetVersion" -ForegroundColor White
    Write-Host " Migrated IDs : $MigratedIds" -ForegroundColor $(if ($MigratedIds -gt 0) { 'Green' } else { 'Gray' })
    Write-Host " Embeddings : $(if ($EmbeddingsRegen) { 'regenerated' } else { 'skipped' })" -ForegroundColor White
    Write-Host " Version : $(if ($VersionBumped) { "$CurrentTaxVersion -> $NewVersion" } else { 'unchanged' })" -ForegroundColor White
    Write-Host ''

    if ($PassThru) {
        [PSCustomObject]@{
            DetectedSchema  = $DetectedVersion
            TargetSchema    = $TargetVersion
            DryRun          = $false
            MigratedIds     = $MigratedIds
            EmbeddingsRegen = $EmbeddingsRegen
            VersionBumped   = $VersionBumped
            NewVersion      = if ($VersionBumped) { $NewVersion } else { $CurrentTaxVersion }
        }
    }
}