Private/Test-KadenPackagePayload.ps1

function Test-KadenPackagePayload {
    <#
    .SYNOPSIS
        Check that the installed payload has its required directories, manifest, and source digests.
    #>

    [CmdletBinding()]
    param(
        [string]$PayloadRoot = (Get-KadenPayloadRoot)
    )

    $requiredDirs = @(
        (Join-Path $PayloadRoot 'scripts')
        # NuGet default excludes omit folders that start with '.'; pack as github/cursor.
        (Join-Path $PayloadRoot 'github')
        (Join-Path $PayloadRoot 'cursor')
        (Join-Path (Join-Path $PayloadRoot 'docs') 'kaden')
        (Join-Path $PayloadRoot 'schemas')
        (Join-Path (Join-Path $PayloadRoot 'templates') 'ai-overlays')
    )
    foreach ($dir in $requiredDirs) {
        if (-not (Test-Path -LiteralPath $dir)) {
            return [pscustomobject]@{
                Valid   = $false
                Message = "KDN-PAYLOAD-INCOMPLETE: Installed Kaden payload is incomplete (missing '$dir'). Repair the module installation and retry."
            }
        }
    }

    $manifestPath = Join-Path $PayloadRoot 'kaden-managed-files.json'
    if (-not (Test-Path -LiteralPath $manifestPath)) {
        return [pscustomobject]@{
            Valid   = $false
            Message = 'KDN-PAYLOAD-INCOMPLETE: Installed Kaden payload is incomplete (missing managed-file manifest). Repair the module installation and retry.'
        }
    }

    try {
        $manifest = Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json
    }
    catch {
        return [pscustomobject]@{
            Valid   = $false
            Message = 'KDN-PAYLOAD-INCOMPLETE: Installed Kaden payload has an invalid managed-file manifest. Repair the module installation and retry.'
        }
    }

    if (-not $manifest.files -or @($manifest.files).Count -lt 1) {
        return [pscustomobject]@{
            Valid   = $false
            Message = 'KDN-PAYLOAD-INCOMPLETE: Installed Kaden payload has an empty managed-file manifest. Repair the module installation and retry.'
        }
    }

    if ([string]$manifest.schemaVersion -ne '1') {
        return [pscustomobject]@{
            Valid   = $false
            Message = 'KDN-PAYLOAD-INCOMPLETE: Installed Kaden payload has an unsupported managed-file manifest schema. Repair the module installation and retry.'
        }
    }

    $sourcePaths = @{}
    $destinationPaths = @{}
    foreach ($entry in @($manifest.files)) {
        $sourceRelativePath = [string]$entry.source
        $destinationRelativePath = [string]$entry.destination
        $digestValue = [string]$entry.digest
        try {
            $sourceKey = ConvertTo-KadenNormalizedManifestPath -RelativePath $sourceRelativePath
            $destinationKey = ConvertTo-KadenNormalizedManifestPath -RelativePath $destinationRelativePath
        }
        catch {
            return [pscustomobject]@{
                Valid   = $false
                Message = "KDN-PAYLOAD-INCOMPLETE: $($_.Exception.Message)"
            }
        }
        if ($sourcePaths.ContainsKey($sourceKey) -or $destinationPaths.ContainsKey($destinationKey)) {
            return [pscustomobject]@{
                Valid   = $false
                Message = 'KDN-PAYLOAD-INCOMPLETE: Installed Kaden payload has duplicate managed-file paths. Repair the module installation and retry.'
            }
        }
        if ([string]$entry.ownership -ne 'module') {
            return [pscustomobject]@{
                Valid   = $false
                Message = 'KDN-PAYLOAD-INCOMPLETE: Installed Kaden payload has a non-module managed-file entry. Repair the module installation and retry.'
            }
        }
        if ($digestValue -notmatch '^[0-9a-f]{64}$') {
            return [pscustomobject]@{
                Valid   = $false
                Message = 'KDN-PAYLOAD-INCOMPLETE: Installed Kaden payload has an invalid managed-file digest. Repair the module installation and retry.'
            }
        }
        $sourcePaths[$sourceKey] = $true
        $destinationPaths[$destinationKey] = $true
        try {
            $source = Resolve-KadenContainedPath -Root $PayloadRoot -RelativePath $sourceRelativePath
        }
        catch {
            return [pscustomobject]@{
                Valid   = $false
                Message = "KDN-PAYLOAD-INCOMPLETE: $($_.Exception.Message)"
            }
        }
        if (-not (Test-Path -LiteralPath $source)) {
            return [pscustomobject]@{
                Valid   = $false
                Message = "KDN-PAYLOAD-INCOMPLETE: Installed Kaden payload is incomplete (missing '$($entry.source)'). Repair the module installation and retry."
            }
        }
        $digest = Get-KadenFileDigest -LiteralPath $source
        if ($digest -ne [string]$entry.digest) {
            return [pscustomobject]@{
                Valid   = $false
                Message = "KDN-PAYLOAD-INCOMPLETE: Installed Kaden payload digest mismatch for '$($entry.source)'. Repair the module installation and retry."
            }
        }
    }

    $expectedPayloadDigest = Get-KadenManifestPayloadDigest -Entries @($manifest.files)
    if ([string]$manifest.payloadDigest -ne $expectedPayloadDigest) {
        return [pscustomobject]@{
            Valid   = $false
            Message = 'KDN-PAYLOAD-INCOMPLETE: Installed Kaden payload digest does not match its managed-file manifest. Repair the module installation and retry.'
        }
    }

    return [pscustomobject]@{
        Valid    = $true
        Message  = 'Payload ready.'
        Manifest = $manifest
    }
}

function ConvertTo-KadenNormalizedManifestPath {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [AllowEmptyString()]
        [string]$RelativePath
    )

    $segments = @($RelativePath -split '[\\/]' | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
    if ($segments.Count -lt 1 -or $segments -contains '.' -or $segments -contains '..') {
        throw "KDN-TXN-PATH-ESCAPE: managed path is not normalized ('$RelativePath')."
    }
    return (($segments -join '/').ToLowerInvariant())
}

function Get-KadenManifestPayloadDigest {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        $Entries
    )

    $lines = @(
        $Entries |
            Sort-Object -Property destination |
            ForEach-Object { [string]$_.destination + "`t" + [string]$_.digest }
    )
    $bytes = [System.Text.Encoding]::UTF8.GetBytes((($lines -join "`n") + "`n"))
    $sha = [System.Security.Cryptography.SHA256]::Create()
    try {
        return ([System.BitConverter]::ToString($sha.ComputeHash($bytes)) -replace '-', '').ToLowerInvariant()
    }
    finally {
        $sha.Dispose()
    }
}