Private/Replacement/Invoke-CgrExistingDestinationReplacement.ps1

function Invoke-CgrExistingDestinationReplacement {
    <#
    .SYNOPSIS
    Archives an existing destination and executes a verified replacement copy.

    .DESCRIPTION
    Internal mutation boundary used only after Copy-GitHubRepository has completed
    ShouldProcess and exact archive-and-replace confirmation. The original
    destination is renamed first, its immutable repository identity is verified,
    and only then is a fresh replacement created and populated from the approved
    plan. Any failure after mutation begins records recovery evidence when possible.

    .NOTES
    This function intentionally does not perform its own ShouldProcess prompt. The
    public command owns user consent; this layer owns ordered mutation, identity
    verification, fail-closed source-state validation, and recovery reporting.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute(
        'PSUseShouldProcessForStateChangingFunctions',
        '',
        Justification = 'The public Copy-GitHubRepository command performs ShouldProcess and exact archive-and-replace confirmation before calling this orchestration boundary.'
    )]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [psobject] $Plan,

        [Parameter(Mandatory)]
        [psobject] $SourceRepository,

        [Parameter(Mandatory)]
        [psobject] $ExistingDestinationRepository,

        [ValidateNotNullOrEmpty()]
        [string] $HostName = 'github.com',

        [string] $ReportPath
    )

    $completedSteps = [System.Collections.Generic.List[object]]::new()
    $archive = $null
    $replacement = $null
    $failureStage = 'ValidateApprovedSourceState'
    $originalRepositoryId = Get-CgrObjectProperty -InputObject $ExistingDestinationRepository -Name 'Id'
    $originalRepositoryNodeId = Get-CgrObjectProperty -InputObject $ExistingDestinationRepository -Name 'NodeId'
    $sourceState = Get-CgrObjectProperty -InputObject $Plan -Name 'SourceState'

    try {
        Assert-CgrApprovedSourceState -Repository $SourceRepository -SourceState $sourceState | Out-Null

        $failureStage = 'PreserveDestinationAsArchive'
        $archive = Rename-CgrGitHubRepository `
            -SourceRepository $ExistingDestinationRepository `
            -ArchiveRepository $Plan.ArchiveRepository `
            -HostName $HostName
        $completedSteps.Add([pscustomobject] @{
                Order = 1
                Name = 'PreserveDestinationAsArchive'
                MutatedGitHub = $true
                Verified = $true
            })

        $failureStage = 'VerifyArchivedDestinationIdentity'
        $archiveRepositoryId = Get-CgrObjectProperty -InputObject $archive -Name 'Id'
        $archiveRepositoryNodeId = Get-CgrObjectProperty -InputObject $archive -Name 'NodeId'
        $idMatches = $null -ne $originalRepositoryId -and $archiveRepositoryId -eq $originalRepositoryId
        $nodeIdMatches = $null -eq $originalRepositoryNodeId -or $null -eq $archiveRepositoryNodeId -or $archiveRepositoryNodeId -eq $originalRepositoryNodeId
        if (-not $idMatches -or -not $nodeIdMatches) {
            $message = "Archived destination '$($archive.FullName)' does not retain the immutable identity of the original destination. Replacement creation was stopped."
            $exception = [System.InvalidOperationException]::new($message)
            throw [System.Management.Automation.ErrorRecord]::new(
                $exception,
                'ArchivedDestinationIdentityMismatch',
                [System.Management.Automation.ErrorCategory]::InvalidResult,
                $archive.FullName
            )
        }
        $completedSteps.Add([pscustomobject] @{
                Order = 2
                Name = 'VerifyArchivedDestinationIdentity'
                MutatedGitHub = $false
                Verified = $true
            })

        $failureStage = 'CreateReplacementRepository'
        $replacement = New-CgrGitHubRepository `
            -Repository $Plan.DestinationRepository `
            -Visibility $Plan.DestinationVisibility `
            -HostName $HostName
        $completedSteps.Add([pscustomobject] @{
                Order = 3
                Name = 'CreateReplacementRepository'
                MutatedGitHub = $true
                Verified = $true
            })

        $failureStage = 'VerifyReplacementRepositoryIdentity'
        $repositoryIdentity = Assert-CgrReplacementRepositoryIdentity `
            -SourceRepository $ExistingDestinationRepository `
            -ArchiveRepository $archive `
            -ReplacementRepository $replacement
        $completedSteps.Add([pscustomobject] @{
                Order = 4
                Name = 'VerifyReplacementRepositoryIdentity'
                MutatedGitHub = $false
                Verified = $true
            })

        $failureStage = "Copy$($Plan.ContentMode)"
        $result = if ($Plan.ContentMode -eq 'FullHistory') {
            Invoke-CgrNewDestinationFullHistory `
                -Plan $Plan `
                -SourceRepository $SourceRepository `
                -DestinationRepository $replacement `
                -HostName $HostName
        }
        else {
            Invoke-CgrNewDestinationSnapshot `
                -Plan $Plan `
                -SourceRepository $SourceRepository `
                -DestinationRepository $replacement `
                -HostName $HostName
        }

        $nextOrder = 5
        foreach ($childStep in @($result.CompletedSteps)) {
            if ($childStep.Name -eq 'CreateDestinationRepository') {
                continue
            }

            $completedSteps.Add([pscustomobject] @{
                    Order = $nextOrder
                    Name = $childStep.Name
                    MutatedGitHub = [bool] $childStep.MutatedGitHub
                    Verified = [bool] $childStep.Verified
                })
            $nextOrder++
        }

        $replacementRepositoryId = $repositoryIdentity.ReplacementRepositoryId
        $replacementRepositoryNodeId = Get-CgrObjectProperty -InputObject $replacement -Name 'NodeId'
        $result | Add-Member -NotePropertyName OriginalDestinationRepository -NotePropertyValue $ExistingDestinationRepository.FullName -Force
        $result | Add-Member -NotePropertyName OriginalDestinationRepositoryId -NotePropertyValue $originalRepositoryId -Force
        $result | Add-Member -NotePropertyName OriginalDestinationRepositoryNodeId -NotePropertyValue $originalRepositoryNodeId -Force
        $result | Add-Member -NotePropertyName ArchiveRepository -NotePropertyValue $archive.FullName -Force
        $result | Add-Member -NotePropertyName ArchiveRepositoryId -NotePropertyValue $repositoryIdentity.ArchiveRepositoryId -Force
        $result | Add-Member -NotePropertyName ArchiveRepositoryNodeId -NotePropertyValue $archiveRepositoryNodeId -Force
        $result | Add-Member -NotePropertyName ArchivedOriginalIdentityPreserved -NotePropertyValue ($repositoryIdentity.ArchiveRepositoryId -eq $repositoryIdentity.SourceRepositoryId) -Force
        $result | Add-Member -NotePropertyName ReplacementDestinationRepository -NotePropertyValue $replacement.FullName -Force
        $result | Add-Member -NotePropertyName ReplacementDestinationRepositoryId -NotePropertyValue $replacementRepositoryId -Force
        $result | Add-Member -NotePropertyName ReplacementDestinationRepositoryNodeId -NotePropertyValue $replacementRepositoryNodeId -Force
        $result | Add-Member -NotePropertyName ReplacementHasDistinctIdentity -NotePropertyValue ($replacementRepositoryId -ne $repositoryIdentity.SourceRepositoryId) -Force
        $result | Add-Member -NotePropertyName DestinationRepositoryId -NotePropertyValue $replacementRepositoryId -Force
        $result | Add-Member -NotePropertyName DestinationRepositoryNodeId -NotePropertyValue $replacementRepositoryNodeId -Force
        $result | Add-Member -NotePropertyName ReplacedDestinationRepositoryId -NotePropertyValue $originalRepositoryId -Force
        $result | Add-Member -NotePropertyName CompletedSteps -NotePropertyValue @($completedSteps) -Force

        if ($ReportPath) {
            $failureStage = 'WriteCompletionReport'
            Write-CgrMigrationExecutionReport -Result $result -Path $ReportPath | Out-Null
        }
        return $result
    }
    catch {
        $recoveryReportPath = $null
        try {
            $recoveryReportPath = Write-CgrExistingDestinationRecoveryReport `
                -Plan $Plan `
                -OriginalDestinationRepositoryId $originalRepositoryId `
                -OriginalDestinationRepositoryNodeId $originalRepositoryNodeId `
                -ArchiveRepository $archive `
                -DestinationRepository $replacement `
                -FailureStage $failureStage `
                -ErrorRecord $_ `
                -CompletedSteps @($completedSteps) `
                -PreferredReportPath $ReportPath
        }
        catch {
            Write-Warning "Existing-destination replacement failed after mutation began, and the recovery report could not be written. Recovery reporting error: $($_.Exception.Message)"
        }

        if ($recoveryReportPath) {
            Write-Warning "Existing-destination replacement failed. The previous destination remains preserved when the archive rename completed. Recovery report: $recoveryReportPath"
        }

        throw
    }
}