Private/New-KadenOperatorModelApplyOutcome.ps1

function Get-KadenOperatorFileFingerprint {
    <#
    .SYNOPSIS
        Read an operator-file hash without throwing.
    #>

    param(
        [Parameter(Mandatory = $true)]
        [string]$LiteralPath
    )

    if (-not (Test-Path -LiteralPath $LiteralPath -PathType Leaf)) {
        return [pscustomobject]@{
            State = 'missing'
            Hash  = $null
        }
    }
    try {
        return [pscustomobject]@{
            State = 'available'
            Hash  = (Get-KadenRawFileSha256 -LiteralPath $LiteralPath)
        }
    } catch {
        return [pscustomobject]@{
            State = 'unreadable'
            Hash  = $null
        }
    }
}

function Test-KadenOperatorFileBytesUnchanged {
    <#
    .SYNOPSIS
        True only when both fingerprints are available and equal, or both are missing.
    #>

    param(
        [Parameter(Mandatory = $true)]$Before,
        [Parameter(Mandatory = $true)]$After
    )

    if ($Before.State -eq 'missing' -and $After.State -eq 'missing') { return $true }
    if ($Before.State -ne 'available' -or $After.State -ne 'available') { return $false }
    return ($Before.Hash -eq $After.Hash)
}

function New-KadenOperatorModelApplyReceipt {
    <#
    .SYNOPSIS
        Build the apply receipt from files the apply step tuned.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Builds an in-memory receipt. It does not change files.')]
    param(
        [Parameter(Mandatory = $true)]
        [AllowEmptyCollection()]
        [object[]]$TunedFiles,

        [Parameter(Mandatory = $true)]
        [string]$OperatorFile
    )

    $entries = New-Object System.Collections.Generic.List[object]
    foreach ($file in @($TunedFiles)) {
        $entries.Add([pscustomobject]@{
                Path          = [string]$file.Path
                Model         = [string]$file.Model
                OperatorInput = [string]$file.OperatorInput
                Source        = 'operator-supplied'
            })
    }
    return [pscustomobject]@{
        Producer     = 'operator-apply'
        DistinctFrom = 'maintainer-sync'
        OperatorFile = $OperatorFile
        Entries      = @($entries.ToArray())
    }
}

function Get-KadenOperatorModelApplyFinalStatus {
    <#
    .SYNOPSIS
        Choose applied, skipped, or failed from the write counts.
    #>

    param(
        [Parameter(Mandatory = $true)][int]$WriteFailures,
        [Parameter(Mandatory = $true)][int]$TunedCount,
        [Parameter(Mandatory = $true)][int]$NotTunedCount,
        [Parameter(Mandatory = $true)][bool]$Changed
    )

    if ($WriteFailures -gt 0 -and $TunedCount -gt 0) {
        return [pscustomobject]@{
            Status = 'failed'
            Reason = 'apply failed partway: tuned files and untouched files are both recorded'
        }
    }
    if ($WriteFailures -gt 0) {
        return [pscustomobject]@{
            Status = 'failed'
            Reason = 'managed agents could not be tuned'
        }
    }
    if ($TunedCount -gt 0 -and $NotTunedCount -gt 0) {
        return [pscustomobject]@{
            Status = 'applied'
            Reason = 'operator choices written where the managed agents are still installed; skipped entries are recorded'
        }
    }
    if ($TunedCount -gt 0 -and $Changed) {
        return [pscustomobject]@{
            Status = 'applied'
            Reason = 'operator choices written to managed agent frontmatter'
        }
    }
    if ($TunedCount -gt 0) {
        return [pscustomobject]@{
            Status = 'applied'
            Reason = 'operator choices already present'
        }
    }
    return [pscustomobject]@{
        Status = 'skipped'
        Reason = 'the latest update no longer installs this agent'
    }
}

function New-KadenOperatorModelApplyOutcome {
    <#
    .SYNOPSIS
        Build the recorded apply outcome, including its receipt.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Builds an in-memory outcome. It does not change files.')]
    param(
        [Parameter(Mandatory = $true)][string]$Status,
        [Parameter(Mandatory = $true)][string]$Reason,
        [Parameter(Mandatory = $true)][string]$Path,
        [Parameter(Mandatory = $true)][string]$OperatorFile,
        [Parameter(Mandatory = $true)][bool]$OperatorFileBytesUnchanged,
        [Parameter(Mandatory = $true)][bool]$Changed,
        [Parameter(Mandatory = $true)][AllowEmptyCollection()][object[]]$Stacks,
        [Parameter(Mandatory = $true)][AllowEmptyCollection()][object[]]$TunedFiles,
        [Parameter(Mandatory = $true)][AllowEmptyCollection()][object[]]$NotTuned
    )

    return [pscustomobject]@{
        Status                     = $Status
        Reason                     = $Reason
        Path                       = $Path
        OperatorFile               = $OperatorFile
        OperatorFileBytesUnchanged = $OperatorFileBytesUnchanged
        Changed                    = $Changed
        Escalation                 = 'none'
        SelectionPrecedence        = 'invocation-override, agent-frontmatter, parent-model'
        Stacks                     = @($Stacks)
        TunedFiles                 = @($TunedFiles)
        NotTuned                   = @($NotTuned)
        Receipt                    = (New-KadenOperatorModelApplyReceipt -TunedFiles $TunedFiles -OperatorFile $OperatorFile)
    }
}