Public/Test-OSDStageDeployment.ps1

function Test-OSDStageDeployment {
    [CmdletBinding()]
    param(
        [string]$WindowsPath,

        [Alias('RequireCache')]
        [switch]$RequireOfflineReady,

        [ValidateRange(0, 1024)]
        [double]$MinimumFreeSpaceGB = 0,

        [switch]$ThrowOnFailure
    )

    $target = Resolve-OSDStageWindowsPath -WindowsPath $WindowsPath
    $stageRoot = Join-Path $target 'Windows\Temp\OSDStage'
    $manifestPath = Join-Path $stageRoot 'DeviceManifest.json'
    $setupCompletePath = Join-Path $target 'Windows\Setup\Scripts\SetupComplete.cmd'
    $checks = [System.Collections.Generic.List[object]]::new()

    function Add-DeploymentCheck {
        param(
            [Parameter(Mandatory)][string]$Name,
            [Parameter(Mandatory)][bool]$Passed,
            [Parameter(Mandatory)][bool]$Required,
            [Parameter(Mandatory)][string]$Message,
            [string]$AppId
        )

        $checks.Add([pscustomobject]@{
            PSTypeName    = 'OSDStage.DeploymentCheck'
            Name          = $Name
            AppId = $AppId
            Required      = $Required
            Passed        = $Passed
            Message       = $Message
        })
    }

    function Test-SafeRelativePath {
        param([string]$Path)

        if ([string]::IsNullOrWhiteSpace($Path) -or
            [IO.Path]::IsPathRooted($Path) -or
            $Path -match '[:*?"<>|\x00-\x1F]') {
            return $false
        }

        $segments = @($Path -split '[\\/]')
        return ($segments.Count -gt 0 -and @($segments | Where-Object { $_ -in @('', '.', '..') }).Count -eq 0)
    }

    function Get-BuiltInPayloadState {
        param([Parameter(Mandatory)]$App)
        if ([string]$App.Type -eq 'OfficeDeploymentTool') {
            $paths = @([string]$App.Setup,[string]$App.Configuration,'BuiltIn\Microsoft365Apps\Office\Data')
            $invalid = @($paths | Where-Object { -not (Test-SafeRelativePath -Path $_) })
            $missing = @($paths | Where-Object { -not (Test-Path -LiteralPath (Join-Path $stageRoot $_)) })
            return [pscustomobject]@{DefinitionValid=($invalid.Count -eq 0);PayloadReady=($invalid.Count -eq 0 -and $missing.Count -eq 0);MissingPaths=$missing}
        }
        try {
            $spec = Get-OSDStageBuiltInPayloadSpec -App $App
            if (-not $spec -or [string]$App.PayloadRoot -ne $spec.RelativeRoot -or [string]$App.PayloadExtension -ne $spec.Extension) {
                throw 'Invalid built-in payload intent.'
            }
            $relative = [string]$App.($spec.Field)
            if ([string]::IsNullOrWhiteSpace($relative)) {
                if ($App.PayloadSha256) { throw 'Pending payload intent must not contain a payload hash.' }
                return [pscustomobject]@{DefinitionValid=$true;PayloadReady=$false;MissingPaths=@("Pending vendor payload: $($spec.RelativeRoot)")}
            }
            if (-not (Test-SafeRelativePath -Path $relative)) { throw 'Unsafe staged payload path.' }
            Assert-OSDStageManifestPayload -App $App -StagedPath $stageRoot | Out-Null
            $root = Join-Path $stageRoot $spec.RelativeRoot
            if ([string]$App.Type -eq 'PrerequisiteExe') {
                $prerequisite = Get-OSDStagePrerequisiteSpec -Family $App.Family -Architecture $App.Architecture -RuntimeType $App.RuntimeType -Channel $App.Channel
                Assert-OSDStagePrerequisiteGeneration -Root $root -Spec $prerequisite | Out-Null
            }
            elseif ([string]$App.Type -eq 'MicrosoftTeamsBootstrapper') {
                if ([string]$App.Setup -ne 'BuiltIn\MicrosoftTeams\teamsbootstrapper.exe' -or
                    -not (Test-Path -LiteralPath (Join-Path $stageRoot $App.Setup) -PathType Leaf)) { throw 'Teams bootstrapper is missing or invalid.' }
            }
            elseif ([string]$App.Type -eq '7ZipExe') { Assert-OSDStage7ZipGeneration -Root $root -Architecture $App.Architecture | Out-Null }
            elseif ([string]$App.Type -eq 'VisualStudioCodeExe') { Assert-OSDStageVisualStudioCodeGeneration -Root $root -Architecture $App.Architecture | Out-Null }
            [pscustomobject]@{DefinitionValid=$true;PayloadReady=$true;MissingPaths=@()}
        }
        catch {
            [pscustomobject]@{DefinitionValid=$false;PayloadReady=$false;MissingPaths=@($_.Exception.Message)}
        }
    }

    function Complete-DeploymentValidation {
        param(
            $Manifest,
            [object[]]$Apps,
            [bool]$OfflineReady,
            [bool]$NetworkRequired,
            $FreeSpaceGB
        )

        $failedRequired = @($checks | Where-Object { $_.Required -and -not $_.Passed })
        $result = [pscustomobject]@{
            PSTypeName       = 'OSDStage.DeploymentValidation'
            WindowsPath      = $target
            StagedPath       = $stageRoot
            ManifestPath     = $manifestPath
            SetupCompletePath= $setupCompletePath
            DeploymentId     = if ($Manifest -and $Manifest.DeploymentId) { [string]$Manifest.DeploymentId } else { $null }
            PackageCount = @($Apps).Count
            Packages     = @($Apps)
            RequireOfflineReady = [bool]$RequireOfflineReady
            OfflineReady     = $OfflineReady
            NetworkRequired  = $NetworkRequired
            FreeSpaceGB      = $FreeSpaceGB
            Checks           = @($checks)
            Valid            = ($failedRequired.Count -eq 0)
        }

        if ($ThrowOnFailure -and -not $result.Valid) {
            $messages = @($failedRequired | ForEach-Object { $_.Message })
            throw "OSDStage deployment validation failed: $($messages -join ' | ')"
        }

        $result
    }

    $manifestExists = Test-Path -LiteralPath $manifestPath -PathType Leaf
    Add-DeploymentCheck -Name 'DeviceManifest' -Passed $manifestExists -Required $true -Message $(
        if ($manifestExists) { 'DeviceManifest.json is present.' } else { "DeviceManifest.json is missing: $manifestPath" }
    )
    if (-not $manifestExists) {
        return (Complete-DeploymentValidation -Manifest $null -Apps @() -OfflineReady $false -NetworkRequired $false -FreeSpaceGB $null)
    }

    $manifest = $null
    try {
        $manifest = Get-Content -LiteralPath $manifestPath -Raw -Encoding UTF8 |
            ConvertFrom-Json -ErrorAction Stop
        Add-DeploymentCheck -Name 'ManifestJson' -Passed $true -Required $true -Message 'DeviceManifest.json contains valid JSON.'
    }
    catch {
        Add-DeploymentCheck -Name 'ManifestJson' -Passed $false -Required $true -Message "DeviceManifest.json is invalid: $($_.Exception.Message)"
        return (Complete-DeploymentValidation -Manifest $null -Apps @() -OfflineReady $false -NetworkRequired $false -FreeSpaceGB $null)
    }

    # Capture the output of the conditional as an array. Arrays emitted inside
    # an if branch are unrolled; a single PSCustomObject has no Count in PS 5.1.
    $apps = @(
        if ($manifest.PSObject.Properties.Name -contains 'Packages') {
            $manifest.Packages | Where-Object { $null -ne $_ }
        }
    )
    $parsedDeploymentId = [guid]::Empty
    $deploymentIdValid = [guid]::TryParse([string]$manifest.DeploymentId, [ref]$parsedDeploymentId)
    Add-DeploymentCheck -Name 'DeploymentId' -Passed $deploymentIdValid -Required $true -Message $(
        if ($deploymentIdValid) { 'DeploymentId is a valid GUID.' } else { 'DeploymentId is missing or invalid; restage an app with the current module.' }
    )

    $runtimeValid = (
        $manifest.Runtime -and
        ([string]$manifest.Runtime.CleanupMode) -in @('OnSuccess', 'Never') -and
        -not [string]::IsNullOrWhiteSpace([string]$manifest.Runtime.LogPath) -and
        -not [string]::IsNullOrWhiteSpace([string]$manifest.Runtime.ResultPath) -and
        -not [string]::IsNullOrWhiteSpace([string]$manifest.Runtime.CacheVolumeLabel) -and
        ($null -eq $manifest.Runtime.PSObject.Properties['PreventSleep'] -or $manifest.Runtime.PreventSleep -is [bool])
    )
    Add-DeploymentCheck -Name 'RuntimeConfiguration' -Passed $runtimeValid -Required $true -Message $(
        if ($runtimeValid) { 'Runtime configuration is complete.' } else { 'Runtime configuration is incomplete or unsupported.' }
    )

    $ids = @($apps | ForEach-Object { [string]$_.Id })
    $validIds = @($ids | Where-Object { $_ -match '^[A-Za-z0-9][A-Za-z0-9._-]*$' -and $_ -notmatch '\.\.' })
    $uniqueIds = @($ids | Select-Object -Unique)
    $queueValid = ($apps.Count -gt 0 -and $validIds.Count -eq $apps.Count -and $uniqueIds.Count -eq $apps.Count)
    Add-DeploymentCheck -Name 'AppQueue' -Passed $queueValid -Required $true -Message $(
        if ($queueValid) { "App queue contains $($apps.Count) unique valid entries." } else { 'App queue is empty, contains duplicate IDs, or contains an invalid ID.' }
    )

    $runtimeFiles = @('Invoke-OSDStagePreInstall.ps1', 'Invoke-OSDStageRunner.ps1', 'Restore-OSDStageCacheGeneration.ps1', 'Assert-OSDStageTransactionSpace.ps1', 'Enter-OSDStageCacheLock.ps1', 'Test-OSDStageOfficeCacheCompatibility.ps1', 'OSDStageSleepPrevention.ps1', 'OSDStagePayloadFile.ps1','OSDStage7Zip.ps1','OSDStageVisualStudioCode.ps1','OSDStagePrerequisite.ps1','OSDStageFont.ps1','OSDStageCertificate.ps1','OSDStageDriver.ps1', 'Invoke-OSDStageBuiltInCacheTransaction.ps1')
    $runtimeMissing = @($runtimeFiles | Where-Object { -not (Test-Path -LiteralPath (Join-Path $stageRoot $_) -PathType Leaf) })
    Add-DeploymentCheck -Name 'RuntimeScripts' -Passed ($runtimeMissing.Count -eq 0) -Required $true -Message $(
        if ($runtimeMissing.Count -eq 0) { 'Standalone runtime scripts and shared cache helpers are staged.' } else { "Missing runtime script(s): $($runtimeMissing -join ', ')." }
    )

    $moduleRoot = Split-Path $PSScriptRoot -Parent
    $mismatchedRuntime = @()
    if ($runtimeMissing.Count -eq 0) {
        foreach ($file in $runtimeFiles) {
            $source = Join-Path $moduleRoot (Join-Path 'Runtime' $file)
            $staged = Join-Path $stageRoot $file
            if (-not (Test-Path -LiteralPath $source -PathType Leaf) -or
                (Get-FileHash -LiteralPath $source -Algorithm SHA256).Hash -ne (Get-FileHash -LiteralPath $staged -Algorithm SHA256).Hash) {
                $mismatchedRuntime += $file
            }
        }
    }
    Add-DeploymentCheck -Name 'RuntimeVersion' -Passed ($runtimeMissing.Count -eq 0 -and $mismatchedRuntime.Count -eq 0) -Required $true -Message $(
        if ($runtimeMissing.Count -eq 0 -and $mismatchedRuntime.Count -eq 0) { 'Staged runtime scripts match the loaded module.' } else { "Staged runtime does not match the loaded module: $($mismatchedRuntime -join ', ')." }
    )

    $setupValid = $false
    $setupMessage = "SetupComplete.cmd is missing: $setupCompletePath"
    if (Test-Path -LiteralPath $setupCompletePath -PathType Leaf) {
        $setupText = Get-Content -LiteralPath $setupCompletePath -Raw -ErrorAction Stop
        $preCount = [regex]::Matches($setupText, '(?m)^:: OSDStage PreInstall\r?$').Count
        $beginCount = [regex]::Matches($setupText, '(?m)^:: OSDStage Begin\r?$').Count
        $endCount = [regex]::Matches($setupText, '(?m)^:: OSDStage End\r?$').Count
        $preIndex = $setupText.IndexOf(':: OSDStage PreInstall', [StringComparison]::Ordinal)
        $beginIndex = $setupText.IndexOf(':: OSDStage Begin', [StringComparison]::Ordinal)
        $endIndex = $setupText.IndexOf(':: OSDStage End', [StringComparison]::Ordinal)
        $setupValid = (
            $preCount -eq 1 -and $beginCount -eq 1 -and $endCount -eq 1 -and
            $preIndex -lt $beginIndex -and $beginIndex -lt $endIndex -and
            $setupText -match 'Invoke-OSDStagePreInstall\.ps1' -and
            $setupText -match 'OSDStage_PREINSTALL_EXITCODE' -and
            $setupText -match 'Invoke-OSDStageRunner\.ps1' -and
            $setupText -match 'OSDStage_EXITCODE'
        )
        $setupMessage = if ($setupValid) { 'SetupComplete contains one ordered, exit-code-guarded OSDStage sequence.' } else { 'SetupComplete OSDStage markers, order, script calls, or exit-code guards are invalid.' }
    }
    Add-DeploymentCheck -Name 'SetupComplete' -Passed $setupValid -Required $true -Message $setupMessage

    $appStates = [System.Collections.Generic.List[object]]::new()
    $allPayloadsReady = $apps.Count -gt 0
    $networkRequired = $false
    foreach ($app in $apps) {
        $id = [string]$app.Id
        $source = [string]$app.Source
        $definitionValid = $false
        $payloadReady = $false
        $integrityValid = $null
        $detail = $null
        $appNetworkRequired = $false

        if ($source -eq 'Repository') {
            $archivePath = Join-Path $stageRoot (Join-Path 'Packages' (Join-Path $id 'Package.zip'))
            $definitionValid = ($app.Payload -and [string]$app.Payload.Sha256 -match '^[A-Fa-f0-9]{64}$')
            if ([string]$app.PackageType -eq 'Certificate' -and [string]$app.CertificateStore -notin @('Root','CA')) { $definitionValid=$false }
            $payloadReady = Test-Path -LiteralPath $archivePath -PathType Leaf
            $integrityValid = $false
            if ($definitionValid -and $payloadReady) {
                $integrityValid = Test-OSDStageFileHash -Path $archivePath -ExpectedSha256 ([string]$app.Payload.Sha256)
            }
            if ($integrityValid -and [string]$app.PackageType -eq 'Certificate') {
                $integrityValid=(Test-OSDStagePackage -PackagePath $archivePath -PackageType Certificate -CertificateStore ([string]$app.CertificateStore)).Valid
            }
            $payloadReady = ($payloadReady -and $integrityValid)
            $detail = if ($payloadReady) { 'Repository archive is present and SHA-256 validated.' } else { 'Repository archive is missing, malformed, or failed SHA-256 validation.' }
        }
        elseif ($source -eq 'BuiltIn' -and $id -eq 'CMTraceOpen') {
            $expectedName = 'CMTrace-Open_{0}_{1}.msi' -f [string]$app.Version,[string]$app.Architecture
            $expectedRelative = 'BuiltIn\CMTraceOpen\' + [string]$app.Architecture + '\' + $expectedName
            $definitionValid = (
                [string]$app.Version -match '^\d+\.\d+\.\d+$' -and
                [string]$app.Type -eq 'VendorMsi' -and
                [string]$app.Package -eq $expectedRelative -and
                [string]$app.Architecture -in @('x64','arm64') -and
                [string]$app.ExpectedSha256 -match '^[A-Fa-f0-9]{64}$'
            )
            $file = Join-Path $stageRoot $expectedRelative
            $payloadReady = $definitionValid -and (Test-Path -LiteralPath $file -PathType Leaf)
            $integrityValid = $payloadReady -and
                ((Get-FileHash -LiteralPath $file -Algorithm SHA256).Hash -eq [string]$app.ExpectedSha256)
            $payloadReady = $payloadReady -and $integrityValid
            $detail = if ($payloadReady) { 'CMTrace Open MSI SHA-256 validated.' } else { 'CMTrace Open MSI missing or hash mismatch.' }
        }
        elseif ($source -eq 'BuiltIn' -and [string]$app.Type -eq 'CompanyPortalAppxBundle') {
            $definitionValid = Test-OSDStageCompanyPortalDefinition -App $app
            $integrityValid = $false
            if ($definitionValid) {
                try { Assert-OSDStageCompanyPortalFiles -App $app -StagedPath $stageRoot; $integrityValid = $true }
                catch { $integrityValid = $false }
            }
            $payloadReady = $definitionValid -and $integrityValid
            $detail = if ($payloadReady) { 'Company Portal bundle, license and dependencies are SHA-256 validated.' } else { 'Company Portal package contents are missing or corrupt.' }
        }
        elseif ($source -eq 'BuiltIn' -and [string]$app.Type -eq 'WindowsFeatureScript') {
            $definitionValid = (
                $id -eq 'WindowsFeatures' -and
                [string]$app.PackageType -eq 'Script' -and
                [string]$app.Script -eq 'BuiltIn\WindowsFeatures\Install.ps1' -and
                [string]$app.Configuration -eq 'BuiltIn\WindowsFeatures\config.json' -and
                [string]$app.ScriptSha256 -match '^[A-Fa-f0-9]{64}$' -and
                [string]$app.ConfigurationSha256 -match '^[A-Fa-f0-9]{64}$'
            )
            $integrityValid = $false
            if ($definitionValid) {
                $scriptFile = Join-Path $stageRoot 'BuiltIn\WindowsFeatures\Install.ps1'
                $configFile = Join-Path $stageRoot 'BuiltIn\WindowsFeatures\config.json'
                if ((Test-Path -LiteralPath $scriptFile -PathType Leaf) -and
                    (Test-Path -LiteralPath $configFile -PathType Leaf)) {
                    $integrityValid = (
                        (Get-FileHash -LiteralPath $scriptFile -Algorithm SHA256).Hash -eq [string]$app.ScriptSha256 -and
                        (Get-FileHash -LiteralPath $configFile -Algorithm SHA256).Hash -eq [string]$app.ConfigurationSha256
                    )
                    if ($integrityValid) {
                        try {
                            $featureConfig = Get-Content -LiteralPath $configFile -Raw -Encoding UTF8 | ConvertFrom-Json -ErrorAction Stop
                            $integrityValid = (
                                [int]$featureConfig.SchemaVersion -eq 2 -and
                                @($featureConfig.Operations).Count -gt 0
                            )
                        }
                        catch { $integrityValid = $false }
                    }
                }
            }
            $payloadReady = $definitionValid -and $integrityValid
            $detail = if ($payloadReady) { 'Windows Features script and configuration are SHA-256 validated.' } else { 'Windows Features definition, content or hash validation failed.' }
        }
        elseif ($source -eq 'BuiltIn' -and [string]$app.Type -eq 'WindowsCapabilityScript') {
            $definitionValid = (
                $id -eq 'WindowsCapabilities' -and [int]$app.WindowsCapabilitySchema -eq 1 -and
                [string]$app.PackageType -eq 'Script' -and
                [string]$app.Script -eq 'BuiltIn\WindowsCapabilities\Install.ps1' -and
                [string]$app.Configuration -eq 'BuiltIn\WindowsCapabilities\config.json' -and
                [string]$app.ScriptSha256 -match '^[A-Fa-f0-9]{64}$' -and
                [string]$app.ConfigurationSha256 -match '^[A-Fa-f0-9]{64}$'
            )
            $integrityValid = $false
            if ($definitionValid) {
                $scriptFile = Join-Path $stageRoot 'BuiltIn\WindowsCapabilities\Install.ps1'
                $configFile = Join-Path $stageRoot 'BuiltIn\WindowsCapabilities\config.json'
                if ((Test-Path -LiteralPath $scriptFile -PathType Leaf) -and
                    (Test-Path -LiteralPath $configFile -PathType Leaf)) {
                    $integrityValid = (
                        (Get-FileHash -LiteralPath $scriptFile -Algorithm SHA256).Hash -eq [string]$app.ScriptSha256 -and
                        (Get-FileHash -LiteralPath $configFile -Algorithm SHA256).Hash -eq [string]$app.ConfigurationSha256
                    )
                    if ($integrityValid) {
                        try {
                            $capabilityConfig = Get-Content -LiteralPath $configFile -Raw -Encoding UTF8 | ConvertFrom-Json -ErrorAction Stop
                            Assert-OSDStageWindowsCapabilityConfiguration -Configuration $capabilityConfig
                            $integrityValid = ([string]$capabilityConfig.DeploymentId -eq [string]$manifest.DeploymentId)
                        }
                        catch { $integrityValid = $false }
                    }
                }
            }
            $payloadReady = $definitionValid -and $integrityValid
            $appNetworkRequired = $payloadReady -and @($capabilityConfig.Operations | Where-Object Action -eq 'Add').Count -gt 0
            if ($appNetworkRequired) { $networkRequired = $true }
            $detail = if ($appNetworkRequired) { 'Capabilities script/config are validated; Add may require Windows Update during SetupComplete. Offline readiness is not guaranteed.' } elseif ($payloadReady) { 'Windows Capabilities script and configuration are SHA-256 validated.' } else { 'Windows Capabilities definition, content or hash validation failed.' }
        }
        elseif ($source -eq 'BuiltIn' -and [string]$app.Type -eq 'DefaultAssociationScript') {
            $definitionValid = (
                $id -eq 'DefaultAssociations' -and [int]$app.DefaultAssociationSchema -eq 1 -and
                [string]$app.PackageType -eq 'Script' -and
                [string]$app.Script -eq 'BuiltIn\DefaultAssociations\Install.ps1' -and
                [string]$app.Configuration -eq 'BuiltIn\DefaultAssociations\config.json' -and
                [string]$app.ScriptSha256 -match '^[A-Fa-f0-9]{64}$' -and
                [string]$app.ConfigurationSha256 -match '^[A-Fa-f0-9]{64}$'
            )
            $integrityValid = $false
            if ($definitionValid) {
                $scriptFile = Join-Path $stageRoot 'BuiltIn\DefaultAssociations\Install.ps1'
                $configFile = Join-Path $stageRoot 'BuiltIn\DefaultAssociations\config.json'
                if ((Test-Path -LiteralPath $scriptFile -PathType Leaf) -and
                    (Test-Path -LiteralPath $configFile -PathType Leaf)) {
                    $integrityValid = (
                        (Get-FileHash -LiteralPath $scriptFile -Algorithm SHA256).Hash -eq [string]$app.ScriptSha256 -and
                        (Get-FileHash -LiteralPath $configFile -Algorithm SHA256).Hash -eq [string]$app.ConfigurationSha256
                    )
                    if ($integrityValid) {
                        try {
                            $featureConfig = Get-Content -LiteralPath $configFile -Raw -Encoding UTF8 | ConvertFrom-Json -ErrorAction Stop
                            Assert-OSDStageDefaultAssociationConfiguration -Configuration $featureConfig
                            $integrityValid = ([string]$featureConfig.DeploymentId -eq [string]$manifest.DeploymentId)
                        }
                        catch { $integrityValid = $false }
                    }
                }
            }
            $payloadReady = $definitionValid -and $integrityValid
            $detail = if ($payloadReady) { 'Default associations script and configuration are SHA-256 validated.' } else { 'Default associations definition, content or hash validation failed.' }
        }
        elseif ($source -eq 'BuiltIn' -and [string]$app.Type -eq 'EnvironmentVariableScript') {
            $definitionValid = (
                $id -eq 'EnvironmentVariables' -and
                [string]$app.PackageType -eq 'Script' -and
                [string]$app.Script -eq 'BuiltIn\EnvironmentVariables\Install.ps1' -and
                [string]$app.Configuration -eq 'BuiltIn\EnvironmentVariables\config.json' -and
                [string]$app.ScriptSha256 -match '^[A-Fa-f0-9]{64}$' -and
                [string]$app.ConfigurationSha256 -match '^[A-Fa-f0-9]{64}$'
            )
            $integrityValid = $false
            if ($definitionValid) {
                $scriptFile = Join-Path $stageRoot 'BuiltIn\EnvironmentVariables\Install.ps1'
                $configFile = Join-Path $stageRoot 'BuiltIn\EnvironmentVariables\config.json'
                if ((Test-Path -LiteralPath $scriptFile -PathType Leaf) -and
                    (Test-Path -LiteralPath $configFile -PathType Leaf)) {
                    $integrityValid = (
                        (Get-FileHash -LiteralPath $scriptFile -Algorithm SHA256).Hash -eq [string]$app.ScriptSha256 -and
                        (Get-FileHash -LiteralPath $configFile -Algorithm SHA256).Hash -eq [string]$app.ConfigurationSha256
                    )
                    if ($integrityValid) {
                        try {
                            $featureConfig = Get-Content -LiteralPath $configFile -Raw -Encoding UTF8 | ConvertFrom-Json -ErrorAction Stop
                            Assert-OSDStageEnvironmentConfiguration -Configuration $featureConfig
                            $integrityValid = ([string]$featureConfig.DeploymentId -eq [string]$manifest.DeploymentId)
                        }
                        catch { $integrityValid = $false }
                    }
                }
            }
            $payloadReady = $definitionValid -and $integrityValid
            $detail = if ($payloadReady) { 'Environment Variables script and configuration are SHA-256 validated.' } else { 'Environment Variables definition, content or hash validation failed.' }
        }
        elseif ($source -eq 'BuiltIn' -and [string]$app.Type -eq 'ServiceScript') {
            $definitionValid = (
                $id -eq 'Services' -and
                [string]$app.PackageType -eq 'Script' -and
                [string]$app.Script -eq 'BuiltIn\Services\Install.ps1' -and
                [string]$app.Configuration -eq 'BuiltIn\Services\config.json' -and
                [string]$app.ScriptSha256 -match '^[A-Fa-f0-9]{64}$' -and
                [string]$app.ConfigurationSha256 -match '^[A-Fa-f0-9]{64}$'
            )
            $integrityValid = $false
            if ($definitionValid) {
                $scriptFile = Join-Path $stageRoot 'BuiltIn\Services\Install.ps1'
                $configFile = Join-Path $stageRoot 'BuiltIn\Services\config.json'
                if ((Test-Path -LiteralPath $scriptFile -PathType Leaf) -and
                    (Test-Path -LiteralPath $configFile -PathType Leaf)) {
                    $integrityValid = (
                        (Get-FileHash -LiteralPath $scriptFile -Algorithm SHA256).Hash -eq [string]$app.ScriptSha256 -and
                        (Get-FileHash -LiteralPath $configFile -Algorithm SHA256).Hash -eq [string]$app.ConfigurationSha256
                    )
                    if ($integrityValid) {
                        try {
                            $featureConfig = Get-Content -LiteralPath $configFile -Raw -Encoding UTF8 | ConvertFrom-Json -ErrorAction Stop
                            Assert-OSDStageServiceConfiguration -Configuration $featureConfig
                            $integrityValid = ([string]$featureConfig.DeploymentId -eq [string]$manifest.DeploymentId)
                        }
                        catch { $integrityValid = $false }
                    }
                }
            }
            $payloadReady = $definitionValid -and $integrityValid
            $detail = if ($payloadReady) { 'Services script and configuration are SHA-256 validated.' } else { 'Services definition, content or hash validation failed.' }
        }
        elseif ($source -eq 'BuiltIn' -and [string]$app.Type -eq 'DesktopSettingsScript') {
            $definitionValid = (
                $id -eq 'DesktopSettings' -and [int]$app.DesktopSettingsSchema -eq 1 -and
                [string]$app.PackageType -eq 'Script' -and
                [string]$app.Script -eq 'BuiltIn\DesktopSettings\Install.ps1' -and
                [string]$app.Configuration -eq 'BuiltIn\DesktopSettings\config.json' -and
                [string]$app.ScriptSha256 -match '^[A-Fa-f0-9]{64}$' -and
                [string]$app.ConfigurationSha256 -match '^[A-Fa-f0-9]{64}$'
            )
            $integrityValid = $false
            if ($definitionValid) {
                $scriptFile = Join-Path $stageRoot 'BuiltIn\DesktopSettings\Install.ps1'
                $configFile = Join-Path $stageRoot 'BuiltIn\DesktopSettings\config.json'
                if ((Test-Path -LiteralPath $scriptFile -PathType Leaf) -and
                    (Test-Path -LiteralPath $configFile -PathType Leaf)) {
                    $integrityValid = (
                        (Get-FileHash -LiteralPath $scriptFile -Algorithm SHA256).Hash -eq [string]$app.ScriptSha256 -and
                        (Get-FileHash -LiteralPath $configFile -Algorithm SHA256).Hash -eq [string]$app.ConfigurationSha256
                    )
                    if ($integrityValid) {
                        try {
                            $featureConfig = Get-Content -LiteralPath $configFile -Raw -Encoding UTF8 | ConvertFrom-Json -ErrorAction Stop
                            Assert-OSDStageDesktopConfiguration -Configuration $featureConfig
                            $integrityValid = ([string]$featureConfig.DeploymentId -eq [string]$manifest.DeploymentId)
                            foreach ($op in @($featureConfig.Operations | Where-Object Kind -eq 'TaskbarPins')) {
                                $layoutFile=Join-Path $target 'Windows\OEM\OSDStage\TaskbarLayoutModification.xml'
                                if (-not (Test-Path -LiteralPath $layoutFile -PathType Leaf) -or
                                    [IO.File]::ReadAllText($layoutFile) -cne (ConvertTo-OSDStageTaskbarXml -Pins @($op.Pins))) { $integrityValid=$false }
                            }
                        }
                        catch { $integrityValid = $false }
                    }
                }
            }
            $payloadReady = $definitionValid -and $integrityValid
            $detail = if ($payloadReady) { 'DesktopSettings script and configuration are SHA-256 validated.' } else { 'DesktopSettings definition, content or hash validation failed.' }
        }
        elseif ($source -eq 'BuiltIn' -and [string]$app.Type -eq 'WiFiProfileScript') {
            $definitionValid = (
                $id -eq 'WiFiProfiles' -and
                [string]$app.PackageType -eq 'Script' -and
                [string]$app.Script -eq 'BuiltIn\WiFiProfiles\Install.ps1' -and
                [string]$app.Configuration -eq 'BuiltIn\WiFiProfiles\config.json' -and
                [string]$app.ScriptSha256 -match '^[A-Fa-f0-9]{64}$' -and
                [string]$app.ConfigurationSha256 -match '^[A-Fa-f0-9]{64}$'
            )
            $integrityValid = $false
            if ($definitionValid) {
                $scriptFile = Join-Path $stageRoot 'BuiltIn\WiFiProfiles\Install.ps1'
                $configFile = Join-Path $stageRoot 'BuiltIn\WiFiProfiles\config.json'
                if ((Test-Path -LiteralPath $scriptFile -PathType Leaf) -and
                    (Test-Path -LiteralPath $configFile -PathType Leaf)) {
                    $integrityValid = (
                        (Get-FileHash -LiteralPath $scriptFile -Algorithm SHA256).Hash -eq [string]$app.ScriptSha256 -and
                        (Get-FileHash -LiteralPath $configFile -Algorithm SHA256).Hash -eq [string]$app.ConfigurationSha256
                    )
                    if ($integrityValid) {
                        try {
                            $featureConfig = Get-Content -LiteralPath $configFile -Raw -Encoding UTF8 | ConvertFrom-Json -ErrorAction Stop
                            $integrityValid = (
                                [int]$featureConfig.SchemaVersion -eq 2 -and
                                @($featureConfig.Operations).Count -gt 0
                            )
                        }
                        catch { $integrityValid = $false }
                    }
                }
            }
            $payloadReady = $definitionValid -and $integrityValid
            $detail = if ($payloadReady) { 'Wi-Fi profiles script and configuration are SHA-256 validated.' } else { 'Wi-Fi profiles definition, content or hash validation failed.' }
        }
        elseif ($source -eq 'BuiltIn' -and [string]$app.Type -eq 'CertificateScript') {
            $definitionValid = (
                $id -eq 'Certificates' -and
                [string]$app.PackageType -eq 'Script' -and
                [string]$app.Script -eq 'BuiltIn\Certificates\Install.ps1' -and
                [string]$app.Configuration -eq 'BuiltIn\Certificates\config.json' -and
                [string]$app.ScriptSha256 -match '^[A-Fa-f0-9]{64}$' -and
                [string]$app.ConfigurationSha256 -match '^[A-Fa-f0-9]{64}$'
            )
            $integrityValid = $false
            if ($definitionValid) {
                $scriptFile = Join-Path $stageRoot 'BuiltIn\Certificates\Install.ps1'
                $configFile = Join-Path $stageRoot 'BuiltIn\Certificates\config.json'
                if ((Test-Path -LiteralPath $scriptFile -PathType Leaf) -and
                    (Test-Path -LiteralPath $configFile -PathType Leaf)) {
                    $integrityValid = (
                        (Get-FileHash -LiteralPath $scriptFile -Algorithm SHA256).Hash -eq [string]$app.ScriptSha256 -and
                        (Get-FileHash -LiteralPath $configFile -Algorithm SHA256).Hash -eq [string]$app.ConfigurationSha256
                    )
                    if ($integrityValid) {
                        try {
                            $featureConfig = Get-Content -LiteralPath $configFile -Raw -Encoding UTF8 | ConvertFrom-Json -ErrorAction Stop
                            $integrityValid = (
                                [int]$featureConfig.SchemaVersion -eq 2 -and
                                @($featureConfig.Operations).Count -gt 0
                            )
                        }
                        catch { $integrityValid = $false }
                    }
                }
            }
            $payloadReady = $definitionValid -and $integrityValid
            $detail = if ($payloadReady) { 'Certificates script and configuration are SHA-256 validated.' } else { 'Certificates definition, content or hash validation failed.' }
        }
        elseif ($source -eq 'BuiltIn' -and [string]$app.Type -eq 'WindowsInboxCleanupScript') {
            $definitionValid = (
                $id -eq 'WindowsInboxCleanup' -and
                [string]$app.PackageType -eq 'Script' -and
                [string]$app.Script -eq 'BuiltIn\WindowsInboxCleanup\Install.ps1' -and
                [string]$app.Configuration -eq 'BuiltIn\WindowsInboxCleanup\config.json' -and
                [string]$app.ScriptSha256 -match '^[A-Fa-f0-9]{64}$' -and
                [string]$app.ConfigurationSha256 -match '^[A-Fa-f0-9]{64}$'
            )
            $integrityValid = $false
            if ($definitionValid) {
                $scriptFile = Join-Path $stageRoot 'BuiltIn\WindowsInboxCleanup\Install.ps1'
                $configFile = Join-Path $stageRoot 'BuiltIn\WindowsInboxCleanup\config.json'
                if ((Test-Path -LiteralPath $scriptFile -PathType Leaf) -and
                    (Test-Path -LiteralPath $configFile -PathType Leaf)) {
                    $integrityValid = (
                        (Get-FileHash -LiteralPath $scriptFile -Algorithm SHA256).Hash -eq [string]$app.ScriptSha256 -and
                        (Get-FileHash -LiteralPath $configFile -Algorithm SHA256).Hash -eq [string]$app.ConfigurationSha256
                    )
                    if ($integrityValid) {
                        try {
                            $cleanupConfig = Get-Content -LiteralPath $configFile -Raw -Encoding UTF8 | ConvertFrom-Json -ErrorAction Stop
                            $integrityValid = ($cleanupConfig.Remove -is [array] -and @($cleanupConfig.Remove).Count -gt 0)
                        }
                        catch { $integrityValid = $false }
                    }
                }
            }
            $payloadReady = $definitionValid -and $integrityValid
            $detail = if ($payloadReady) { 'Bundled script and configuration are SHA-256 validated.' } else { 'Bundled script definition, content or hash validation failed.' }
        }
        elseif ($source -eq 'BuiltIn') {
            $state = Get-BuiltInPayloadState -App $app
            $definitionValid = $state.DefinitionValid
            $payloadReady = $state.PayloadReady
            if ($definitionValid -and -not $payloadReady) { $networkRequired = $true }
            $detail = if ($payloadReady) { 'Complete built-in payload is staged locally.' } elseif ($definitionValid) { "Built-in acquisition is required for: $($state.MissingPaths -join ', ')." } else { 'Built-in manifest paths or type are invalid.' }
        }
        else {
            $detail = "Unsupported app source '$source'."
        }

        $offlinePayloadReady = $payloadReady -and -not $appNetworkRequired
        if (-not $offlinePayloadReady) { $allPayloadsReady = $false }
        $appRequiredStateValid = ($definitionValid -and (($source -ne 'Repository' -and [string]$app.Type -notin @('WindowsInboxCleanupScript','WindowsFeatureScript','WindowsCapabilityScript','DefaultAssociationScript','EnvironmentVariableScript','ServiceScript','DesktopSettingsScript','WiFiProfileScript','CertificateScript','CompanyPortalAppxBundle') -and $id -ne 'CMTraceOpen') -or $payloadReady))
        Add-DeploymentCheck -Name 'AppDefinition' -AppId $id -Passed $appRequiredStateValid -Required $true -Message "${id}: $detail"
        Add-DeploymentCheck -Name 'OfflinePayload' -AppId $id -Passed $offlinePayloadReady -Required ([bool]$RequireOfflineReady) -Message "${id}: $detail"

        $appStates.Add([pscustomobject]@{
            Id              = $id
            Source          = $source
            Type            = [string]$app.Type
            Version         = if ($app.Version) { [string]$app.Version } else { $null }
            Architecture    = if ($app.Architecture) { [string]$app.Architecture } else { $null }
            DefinitionValid = $definitionValid
            PayloadReady    = $payloadReady
            IntegrityValid  = $integrityValid
            Detail          = $detail
        })
    }

    $freeSpaceGB = $null
    try {
        $driveRoot = [IO.Path]::GetPathRoot([IO.Path]::GetFullPath($target))
        $drive = Get-PSDrive -Name $driveRoot.TrimEnd('\').TrimEnd(':') -ErrorAction Stop
        $freeSpaceGB = [math]::Round([double]$drive.Free / 1GB, 2)
        $freeSpaceValid = ($MinimumFreeSpaceGB -le 0 -or $freeSpaceGB -ge $MinimumFreeSpaceGB)
        Add-DeploymentCheck -Name 'FreeSpace' -Passed $freeSpaceValid -Required ($MinimumFreeSpaceGB -gt 0) -Message $(
            if ($freeSpaceValid) { "Offline Windows volume has $freeSpaceGB GB free." } else { "Offline Windows volume has $freeSpaceGB GB free; $MinimumFreeSpaceGB GB is required." }
        )
    }
    catch {
        Add-DeploymentCheck -Name 'FreeSpace' -Passed $false -Required ($MinimumFreeSpaceGB -gt 0) -Message "Free space could not be determined: $($_.Exception.Message)"
    }

    Complete-DeploymentValidation `
        -Manifest $manifest `
        -Apps @($appStates) `
        -OfflineReady $allPayloadsReady `
        -NetworkRequired $networkRequired `
        -FreeSpaceGB $freeSpaceGB
}