Private/Get-Win32ToolkitDepsCheckpointName.ps1

function Get-Win32ToolkitDepsCheckpointName {
    <#
    .SYNOPSIS
        Computes the deterministic 'clean-base+deps-<key>' checkpoint name for a project's staged deps.
    .DESCRIPTION
        The key is a SHA256 (first 12 hex chars) over everything that makes the dependency layer what it
        is — so a change in ANY of these yields a different name, and the stale checkpoint is simply
        never matched again:
          * the staged dependencies.json content (ids, order, silent args),
          * the SHA256 of every staged installer/PSADT payload file (a version bump of an unpinned
            'latest' dependency changes the bytes => new key),
          * the PARENT checkpoint's identity (InstanceId + creation time): a deps checkpoint must never
            outlive the clean-base image it was built on. (In practice VM maintenance deletes all
            checkpoints anyway — this key component is belt-and-braces per the design review.)
        Returns $null when the project has no staged dependencies (no manifest), when the parent
        checkpoint cannot be resolved, or on any error — callers treat $null as "feature not applicable".
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$ProjectPath,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$VMName,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$ParentCheckpointName
    )

    try {
        $manifest = Join-Path $ProjectPath 'Sandbox\Dependencies\dependencies.json'
        if (-not (Test-Path -LiteralPath $manifest)) { return $null }

        $parent = Get-VMCheckpoint -VMName $VMName -Name $ParentCheckpointName -ErrorAction SilentlyContinue
        if (-not $parent) { return $null }

        $sb = New-Object System.Text.StringBuilder
        [void]$sb.Append((Get-Content -LiteralPath $manifest -Raw))
        [void]$sb.Append("|parent:$($parent.Id)|$($parent.CreationTime.ToString('o'))")

        # Every staged payload file, in a deterministic order. dependencies.json is hashed as CONTENT
        # above; any marker/metadata files (e.g. a legacy .staged.json with its per-restage timestamp)
        # are excluded — the key must rotate only when the PAYLOAD or parent image changes.
        $depRoot = Join-Path $ProjectPath 'Sandbox\Dependencies'
        $files = @(Get-ChildItem -LiteralPath $depRoot -Recurse -File -ErrorAction SilentlyContinue |
            Where-Object { $_.Name -ne 'dependencies.json' -and $_.Name -ne '.staged.json' } | Sort-Object FullName)
        foreach ($f in $files) {
            $h = (Get-FileHash -LiteralPath $f.FullName -Algorithm SHA256 -ErrorAction Stop).Hash
            [void]$sb.Append("|$($f.FullName.Substring($depRoot.Length))=$h")
        }

        $bytes  = [System.Text.Encoding]::UTF8.GetBytes($sb.ToString())
        $sha    = [System.Security.Cryptography.SHA256]::Create()
        try { $keyHex = ([System.BitConverter]::ToString($sha.ComputeHash($bytes)) -replace '-', '').Substring(0, 12).ToLowerInvariant() }
        finally { $sha.Dispose() }

        return "clean-base+deps-$keyHex"
    }
    catch {
        Write-Verbose "Deps-checkpoint key unavailable: $($_.Exception.Message)"
        return $null
    }
}