Private/Migration/New-CgrSnapshotReleaseCheckpointPlan.ps1

function New-CgrSnapshotReleaseCheckpointPlan {
    <#
    .SYNOPSIS
    Builds immutable Snapshot release-checkpoint evidence from an approved release selection.

    .DESCRIPTION
    Consumes the already-resolved release selection, captures tag-reference and repository-tree
    evidence for each selected release, coalesces duplicate peeled commit targets, and derives
    the checkpoint sequence exclusively from Git ancestry. The helper is read-only and never
    re-runs release filtering or selection.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute(
        'PSUseShouldProcessForStateChangingFunctions',
        '',
        Justification = 'This private helper captures read-only planning evidence and performs no mutation.'
    )]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [psobject] $Repository,

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

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

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

    $releaseEvidence = [System.Collections.Generic.List[object]]::new()
    $selectedReleases = @($ReleaseSelection.Releases)
    $treeByCommit = @{}

    for ($selectionIndex = 0; $selectionIndex -lt $selectedReleases.Count; $selectionIndex++) {
        $release = $selectedReleases[$selectionIndex]
        $tagName = [string] $release.TagName
        $peeledCommitSha = [string] $release.TargetCommitSha

        $tagRefResult = Invoke-CgrGitHubApiReadRequest `
            -ArgumentList @('api', '--hostname', $HostName, "repos/$($Repository.FullName)/git/ref/tags/$([uri]::EscapeDataString($tagName))")
        if ($tagRefResult.ExitCode -ne 0) {
            $diagnostic = Protect-CgrDiagnosticText -Text ([string] $tagRefResult.ErrorText)
            $message = "Selected release tag '$tagName' could not be read while building Snapshot checkpoint evidence. The tag may have been deleted or moved. $diagnostic"
            $exception = [System.InvalidOperationException]::new($message.Trim())
            $errorRecord = [System.Management.Automation.ErrorRecord]::new(
                $exception,
                'SnapshotReleaseCheckpointTagReadFailed',
                [System.Management.Automation.ErrorCategory]::ReadError,
                $tagName
            )
            $PSCmdlet.ThrowTerminatingError($errorRecord)
        }

        $tagRefJson = ($tagRefResult.Output | ForEach-Object { [string] $_ }) -join "`n"
        $tagRef = $tagRefJson | ConvertFrom-Json -Depth 20
        $tagObjectType = [string] $tagRef.object.type
        $tagObjectSha = [string] $tagRef.object.sha
        if ($tagObjectType -notin @('commit', 'tag') -or [string]::IsNullOrWhiteSpace($tagObjectSha)) {
            $message = "Selected release tag '$tagName' does not resolve through a supported lightweight or annotated Git tag reference."
            $exception = [System.InvalidOperationException]::new($message)
            $errorRecord = [System.Management.Automation.ErrorRecord]::new(
                $exception,
                'SnapshotReleaseCheckpointTagUnsupported',
                [System.Management.Automation.ErrorCategory]::InvalidData,
                $tagName
            )
            $PSCmdlet.ThrowTerminatingError($errorRecord)
        }

        if (-not $treeByCommit.ContainsKey($peeledCommitSha)) {
            $treeResult = Invoke-CgrGitHubApiReadRequest `
                -ArgumentList @('api', '--hostname', $HostName, "repos/$($Repository.FullName)/commits/$peeledCommitSha", '--jq', '.commit.tree.sha')
            if ($treeResult.ExitCode -ne 0) {
                $diagnostic = Protect-CgrDiagnosticText -Text ([string] $treeResult.ErrorText)
                $message = "Selected release tag '$tagName' resolved to commit '$peeledCommitSha', but its repository tree could not be captured for Snapshot checkpoint planning. $diagnostic"
                $exception = [System.InvalidOperationException]::new($message.Trim())
                $errorRecord = [System.Management.Automation.ErrorRecord]::new(
                    $exception,
                    'SnapshotReleaseCheckpointTreeReadFailed',
                    [System.Management.Automation.ErrorCategory]::ReadError,
                    $peeledCommitSha
                )
                $PSCmdlet.ThrowTerminatingError($errorRecord)
            }

            $treeSha = [string] @($treeResult.Output)[0]
            if ([string]::IsNullOrWhiteSpace($treeSha)) {
                $message = "Selected release commit '$peeledCommitSha' did not return a tree SHA required for immutable Snapshot checkpoint evidence."
                $exception = [System.InvalidOperationException]::new($message)
                $errorRecord = [System.Management.Automation.ErrorRecord]::new(
                    $exception,
                    'SnapshotReleaseCheckpointTreeMissing',
                    [System.Management.Automation.ErrorCategory]::InvalidData,
                    $peeledCommitSha
                )
                $PSCmdlet.ThrowTerminatingError($errorRecord)
            }
            $treeByCommit[$peeledCommitSha] = $treeSha
        }

        $releaseEvidence.Add([pscustomobject] @{
                SelectionOrder = $selectionIndex + 1
                ReleaseId = $release.ReleaseId
                TagName = $tagName
                TagObjectType = $tagObjectType
                TagObjectSha = $tagObjectSha
                PeeledCommitSha = $peeledCommitSha
                TreeSha = [string] $treeByCommit[$peeledCommitSha]
            })
    }

    $distinctCommitShas = @($releaseEvidence.ToArray() | ForEach-Object { $_.PeeledCommitSha } | Select-Object -Unique)
    $ancestorCountByCommit = @{}
    foreach ($commitSha in $distinctCommitShas) {
        $ancestorCountByCommit[$commitSha] = 0
    }

    for ($leftIndex = 0; $leftIndex -lt $distinctCommitShas.Count; $leftIndex++) {
        for ($rightIndex = $leftIndex + 1; $rightIndex -lt $distinctCommitShas.Count; $rightIndex++) {
            $leftSha = [string] $distinctCommitShas[$leftIndex]
            $rightSha = [string] $distinctCommitShas[$rightIndex]
            $compareResult = Invoke-CgrGitHubApiReadRequest `
                -ArgumentList @('api', '--hostname', $HostName, "repos/$($Repository.FullName)/compare/$leftSha...$rightSha", '--jq', '.status')
            if ($compareResult.ExitCode -ne 0) {
                $diagnostic = Protect-CgrDiagnosticText -Text ([string] $compareResult.ErrorText)
                $message = "Git ancestry could not be compared for selected Snapshot release commits '$leftSha' and '$rightSha'. Planning cannot invent checkpoint order. $diagnostic"
                $exception = [System.InvalidOperationException]::new($message.Trim())
                $errorRecord = [System.Management.Automation.ErrorRecord]::new(
                    $exception,
                    'SnapshotReleaseCheckpointTopologyReadFailed',
                    [System.Management.Automation.ErrorCategory]::ReadError,
                    "$leftSha...$rightSha"
                )
                $PSCmdlet.ThrowTerminatingError($errorRecord)
            }

            $status = [string] @($compareResult.Output)[0]
            switch ($status) {
                'ahead' { $ancestorCountByCommit[$rightSha] = [int] $ancestorCountByCommit[$rightSha] + 1 }
                'behind' { $ancestorCountByCommit[$leftSha] = [int] $ancestorCountByCommit[$leftSha] + 1 }
                default {
                    $leftTags = @($releaseEvidence | Where-Object PeeledCommitSha -EQ $leftSha | ForEach-Object TagName) -join ', '
                    $rightTags = @($releaseEvidence | Where-Object PeeledCommitSha -EQ $rightSha | ForEach-Object TagName) -join ', '
                    $message = "Selected Snapshot release topology is incompatible: '$leftTags' ($leftSha) and '$rightTags' ($rightSha) are not on one comparable ancestry chain (Git compare status '$status'). Narrow the release selection to one linear release line."
                    $exception = [System.InvalidOperationException]::new($message)
                    $errorRecord = [System.Management.Automation.ErrorRecord]::new(
                        $exception,
                        'SnapshotReleaseCheckpointTopologyIncompatible',
                        [System.Management.Automation.ErrorCategory]::InvalidData,
                        "$leftSha...$rightSha"
                    )
                    $PSCmdlet.ThrowTerminatingError($errorRecord)
                }
            }
        }
    }

    $orderedCommitShas = @($distinctCommitShas | Sort-Object -Property @{ Expression = { [int] $ancestorCountByCommit[[string] $_] }; Ascending = $true })
    $checkpoints = [System.Collections.Generic.List[object]]::new()
    for ($checkpointIndex = 0; $checkpointIndex -lt $orderedCommitShas.Count; $checkpointIndex++) {
        $commitSha = [string] $orderedCommitShas[$checkpointIndex]
        $boundaryReleases = @($releaseEvidence | Where-Object PeeledCommitSha -EQ $commitSha | Sort-Object SelectionOrder)
        $checkpoints.Add([pscustomobject] @{
                Order = $checkpointIndex + 1
                SourceCommitSha = $commitSha
                SourceTreeSha = [string] $treeByCommit[$commitSha]
                ReleaseIds = @($boundaryReleases.ReleaseId)
                TagNames = @($boundaryReleases.TagName)
                SelectionOrders = @($boundaryReleases.SelectionOrder)
            })
    }

    $finalHeadCheckpointRequired = $true
    if ($checkpoints.Count -gt 0) {
        $finalCheckpoint = $checkpoints[$checkpoints.Count - 1]
        $finalHeadCheckpointRequired = [string] $finalCheckpoint.SourceTreeSha -ne [string] $SourceState.TreeSha

        if ($finalHeadCheckpointRequired -and [string] $finalCheckpoint.SourceCommitSha -ne [string] $SourceState.CommitSha) {
            $headCompareResult = Invoke-CgrGitHubApiReadRequest `
                -ArgumentList @('api', '--hostname', $HostName, "repos/$($Repository.FullName)/compare/$($finalCheckpoint.SourceCommitSha)...$($SourceState.CommitSha)", '--jq', '.status')
            if ($headCompareResult.ExitCode -ne 0) {
                $diagnostic = Protect-CgrDiagnosticText -Text ([string] $headCompareResult.ErrorText)
                $message = "The final selected release commit '$($finalCheckpoint.SourceCommitSha)' could not be compared with reviewed default-branch HEAD '$($SourceState.CommitSha)'. Planning cannot safely append current HEAD state. $diagnostic"
                $exception = [System.InvalidOperationException]::new($message.Trim())
                $errorRecord = [System.Management.Automation.ErrorRecord]::new(
                    $exception,
                    'SnapshotReleaseCheckpointHeadTopologyReadFailed',
                    [System.Management.Automation.ErrorCategory]::ReadError,
                    $SourceState.CommitSha
                )
                $PSCmdlet.ThrowTerminatingError($errorRecord)
            }

            $headStatus = [string] @($headCompareResult.Output)[0]
            if ($headStatus -ne 'ahead') {
                $message = "The final selected Snapshot release commit '$($finalCheckpoint.SourceCommitSha)' is not an ancestor of reviewed default-branch HEAD '$($SourceState.CommitSha)' (Git compare status '$headStatus'). Planning cannot fabricate a release-to-HEAD checkpoint progression."
                $exception = [System.InvalidOperationException]::new($message)
                $errorRecord = [System.Management.Automation.ErrorRecord]::new(
                    $exception,
                    'SnapshotReleaseCheckpointHeadTopologyIncompatible',
                    [System.Management.Automation.ErrorCategory]::InvalidData,
                    $SourceState.CommitSha
                )
                $PSCmdlet.ThrowTerminatingError($errorRecord)
            }
        }
    }

    [pscustomobject] @{
        PSTypeName = 'CopyGitHubRepo.SnapshotReleaseCheckpointPlan'
        SchemaVersion = 1
        Repository = $Repository.FullName
        SelectedReleaseCount = $selectedReleases.Count
        CheckpointCount = $checkpoints.Count
        PlannedSnapshotCommitCount = $checkpoints.Count + $(if ($finalHeadCheckpointRequired) { 1 } else { 0 })
        ReleaseEvidence = $releaseEvidence.ToArray()
        Checkpoints = $checkpoints.ToArray()
        SourceHead = [pscustomobject] @{
            DefaultBranch = $SourceState.DefaultBranch
            CommitSha = $SourceState.CommitSha
            TreeSha = $SourceState.TreeSha
        }
        FinalHeadCheckpointRequired = [bool] $finalHeadCheckpointRequired
    }
}