Support/Package/Schema/Eigenverft.Manifested.Sandbox.Package.DefinitionSchema.ps1

<#
    Eigenverft.Manifested.Sandbox.Package.DefinitionSchema
    Package definition JSON validation (version-dispatched) and release default merge helpers.
#>


$script:PackageDefinitionSupportedSchemaVersions = @(
    '1.0'
)

function Assert-PackageDefinitionSchemaVersionSupported {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$SchemaVersionText,

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

    foreach ($supported in $script:PackageDefinitionSupportedSchemaVersions) {
        if ([string]::Equals($SchemaVersionText, $supported, [System.StringComparison]::Ordinal)) {
            return
        }
    }

    $supportedList = ($script:PackageDefinitionSupportedSchemaVersions | ForEach-Object { "'$_'" }) -join ', '
    throw "Package definition '$DefinitionDocumentPath' uses unsupported schemaVersion '$SchemaVersionText'. Supported schemaVersion values are $supportedList."
}

function Assert-PackageDefinitionSchema {
<#
.SYNOPSIS
Validates the Package definition schema for this package pass.
 
.DESCRIPTION
Performs cross-version checks (retired names, required top-level fields,
schemaVersion) then dispatches to a schema-specific validator. Today only
schemaVersion '1.0' is implemented; additional versions add new branches
without changing callers.
 
.PARAMETER DefinitionDocumentInfo
The loaded Package definition document info.
 
.PARAMETER DefinitionId
The expected definition id.
 
.EXAMPLE
Assert-PackageDefinitionSchema -DefinitionDocumentInfo $definitionInfo -DefinitionId VSCodeRuntime
#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$DefinitionDocumentInfo,

        [Parameter(Mandatory = $true)]
        [string]$DefinitionId,

        [string]$DefinitionRepositoryId = (Get-PackageDefaultRepositoryId)
    )

    $definition = $DefinitionDocumentInfo.Document
    foreach ($retiredProperty in @('classification', 'target', 'origins', 'interfaces', 'packageType', 'paths', 'sources', 'packages', 'entryPoints', 'packageFamily', 'managedPaths')) {
        if ($definition.PSObject.Properties[$retiredProperty]) {
            throw "Package definition '$($DefinitionDocumentInfo.Path)' still uses retired property '$retiredProperty'."
        }
    }

    foreach ($requiredProperty in @('schemaVersion', 'id', 'display', 'upstreamSources', 'providedTools', 'releaseDefaults', 'releases')) {
        if (-not $definition.PSObject.Properties[$requiredProperty]) {
            throw "Package definition '$($DefinitionDocumentInfo.Path)' is missing required property '$requiredProperty'."
        }
    }
    $schemaVersionText = [string]$definition.schemaVersion
    if ([string]::IsNullOrWhiteSpace($schemaVersionText)) {
        throw "Package definition '$($DefinitionDocumentInfo.Path)' defines schemaVersion, but it is empty."
    }
    Assert-PackageDefinitionSchemaVersionSupported -SchemaVersionText $schemaVersionText -DefinitionDocumentPath $DefinitionDocumentInfo.Path

    switch -Exact ($schemaVersionText) {
        '1.0' {
            Assert-PackageDefinitionSchema_1_0 -DefinitionDocumentInfo $DefinitionDocumentInfo -DefinitionId $DefinitionId -DefinitionRepositoryId $DefinitionRepositoryId
            return
        }
        default {
            throw "Package definition '$($DefinitionDocumentInfo.Path)' encountered unsupported schemaVersion '$schemaVersionText' after validation gate."
        }
    }
}

function Assert-PackageDefinitionSchema_1_0 {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$DefinitionDocumentInfo,

        [Parameter(Mandatory = $true)]
        [string]$DefinitionId,

        [string]$DefinitionRepositoryId = (Get-PackageDefaultRepositoryId)
    )

    $definition = $DefinitionDocumentInfo.Document

    if (-not [string]::Equals([string]$definition.id, [string]$DefinitionId, [System.StringComparison]::Ordinal)) {
        throw "Package definition id '$($definition.id)' does not match requested definition id '$DefinitionId'."
    }

    if ($definition.PSObject.Properties['dependencies']) {
        foreach ($dependency in @($definition.dependencies)) {
            if (-not $dependency.PSObject.Properties['definitionId'] -or [string]::IsNullOrWhiteSpace([string]$dependency.definitionId)) {
                throw "Package definition '$($definition.id)' has dependency without definitionId."
            }
            $dependencyRepositoryId = $DefinitionRepositoryId
            if ($dependency.PSObject.Properties['repositoryId']) {
                if ([string]::IsNullOrWhiteSpace([string]$dependency.repositoryId)) {
                    throw "Package definition '$($definition.id)' has dependency '$($dependency.definitionId)' with empty repositoryId."
                }
                $dependencyRepositoryId = [string]$dependency.repositoryId
            }
            if ([string]::Equals([string]$dependency.definitionId, [string]$definition.id, [System.StringComparison]::OrdinalIgnoreCase) -and
                [string]::Equals($dependencyRepositoryId, $DefinitionRepositoryId, [System.StringComparison]::OrdinalIgnoreCase)) {
                throw "Package definition '$($definition.id)' cannot depend on itself."
            }
        }
    }

    foreach ($upstreamSourceProperty in @($definition.upstreamSources.PSObject.Properties)) {
        $upstreamSource = $upstreamSourceProperty.Value
        if (-not $upstreamSource.PSObject.Properties['kind'] -or [string]::IsNullOrWhiteSpace([string]$upstreamSource.kind)) {
            throw "Package definition '$($definition.id)' has upstream source '$($upstreamSourceProperty.Name)' without kind."
        }

        switch -Exact ([string]$upstreamSource.kind) {
            'download' {
                if (-not $upstreamSource.PSObject.Properties['baseUri'] -or [string]::IsNullOrWhiteSpace([string]$upstreamSource.baseUri)) {
                    throw "Package definition '$($definition.id)' has download upstream source '$($upstreamSourceProperty.Name)' without baseUri."
                }
            }
            'githubRelease' {
                if (-not $upstreamSource.PSObject.Properties['repositoryOwner'] -or [string]::IsNullOrWhiteSpace([string]$upstreamSource.repositoryOwner)) {
                    throw "Package definition '$($definition.id)' has GitHub release upstream source '$($upstreamSourceProperty.Name)' without repositoryOwner."
                }
                if (-not $upstreamSource.PSObject.Properties['repositoryName'] -or [string]::IsNullOrWhiteSpace([string]$upstreamSource.repositoryName)) {
                    throw "Package definition '$($definition.id)' has GitHub release upstream source '$($upstreamSourceProperty.Name)' without repositoryName."
                }
            }
            default {
                throw "Package definition '$($definition.id)' uses unsupported upstream source kind '$($upstreamSource.kind)' for '$($upstreamSourceProperty.Name)'."
            }
        }
    }

    if ($definition.releaseDefaults.PSObject.Properties['requirements']) {
        throw "Package definition '$($definition.id)' still uses retired property 'releaseDefaults.requirements'. Use 'releaseDefaults.compatibility.checks'."
    }

    foreach ($requiredDefaultProperty in @('compatibility', 'install', 'validation', 'existingInstallDiscovery', 'existingInstallPolicy')) {
        if (-not $definition.releaseDefaults.PSObject.Properties[$requiredDefaultProperty]) {
            throw "Package definition '$($definition.id)' is missing releaseDefaults.$requiredDefaultProperty."
        }
    }
    foreach ($retiredDefaultProperty in @('existingInstall')) {
        if ($definition.releaseDefaults.PSObject.Properties[$retiredDefaultProperty]) {
            throw "Package definition '$($definition.id)' still uses retired property 'releaseDefaults.$retiredDefaultProperty'."
        }
    }

    foreach ($release in @($definition.releases)) {
        foreach ($retiredProperty in @('artifact', 'acquisitions', 'sourceOptions', 'reuse', 'channel')) {
            if ($release.PSObject.Properties[$retiredProperty]) {
                throw "Package release '$($release.id)' in '$($definition.id)' still uses retired property '$retiredProperty'."
            }
        }
        if ($release.PSObject.Properties['requirements']) {
            throw "Package release '$($release.id)' in '$($definition.id)' still uses retired property 'requirements'. Use 'compatibility.checks'."
        }
        foreach ($retiredReleaseProperty in @('existingInstall')) {
            if ($release.PSObject.Properties[$retiredReleaseProperty]) {
                throw "Package release '$($release.id)' in '$($definition.id)' still uses retired property '$retiredReleaseProperty'."
            }
        }

        foreach ($requiredProperty in @('id', 'version', 'releaseTrack', 'flavor', 'constraints')) {
            if (-not $release.PSObject.Properties[$requiredProperty]) {
                throw "Package release '$($release.id)' in '$($definition.id)' is missing required property '$requiredProperty'."
            }
        }

        $effectiveRelease = Resolve-PackageEffectiveRelease -Definition $definition -Release $release
        foreach ($requiredEffectiveProperty in @('install', 'validation', 'compatibility', 'existingInstallDiscovery', 'existingInstallPolicy')) {
            if (-not $effectiveRelease.PSObject.Properties[$requiredEffectiveProperty]) {
                throw "Package release '$($release.id)' in '$($definition.id)' is missing required effective property '$requiredEffectiveProperty'."
            }
        }
        if ($effectiveRelease.compatibility.PSObject.Properties['packages']) {
            throw "Package release '$($release.id)' in '$($definition.id)' still uses retired property 'compatibility.packages'. Use 'compatibility.checks'."
        }
        if (-not $effectiveRelease.compatibility.PSObject.Properties['checks']) {
            throw "Package release '$($release.id)' in '$($definition.id)' is missing compatibility.checks."
        }
        foreach ($compatibilityCheck in @($effectiveRelease.compatibility.checks)) {
            if ($null -eq $compatibilityCheck) {
                continue
            }
            if (-not $compatibilityCheck.PSObject.Properties['kind'] -or [string]::IsNullOrWhiteSpace([string]$compatibilityCheck.kind)) {
                throw "Package release '$($release.id)' in '$($definition.id)' has a compatibility check without kind."
            }
            $onFail = 'fail'
            if ($compatibilityCheck.PSObject.Properties['onFail'] -and -not [string]::IsNullOrWhiteSpace([string]$compatibilityCheck.onFail)) {
                $onFail = ([string]$compatibilityCheck.onFail).ToLowerInvariant()
            }
            if ($onFail -notin @('fail', 'warn')) {
                throw "Package release '$($release.id)' in '$($definition.id)' uses unsupported compatibility onFail '$($compatibilityCheck.onFail)'."
            }

            switch -Exact ([string]$compatibilityCheck.kind) {
                'osFamily' {
                    $hasAllowed = $compatibilityCheck.PSObject.Properties['allowed'] -and @($compatibilityCheck.allowed).Count -gt 0
                    $hasBlocked = $compatibilityCheck.PSObject.Properties['blocked'] -and @($compatibilityCheck.blocked).Count -gt 0
                    if (-not $hasAllowed -and -not $hasBlocked) {
                        throw "Package release '$($release.id)' in '$($definition.id)' has an osFamily compatibility check without allowed or blocked values."
                    }
                }
                'cpuArchitecture' {
                    $hasAllowed = $compatibilityCheck.PSObject.Properties['allowed'] -and @($compatibilityCheck.allowed).Count -gt 0
                    $hasBlocked = $compatibilityCheck.PSObject.Properties['blocked'] -and @($compatibilityCheck.blocked).Count -gt 0
                    if (-not $hasAllowed -and -not $hasBlocked) {
                        throw "Package release '$($release.id)' in '$($definition.id)' has a cpuArchitecture compatibility check without allowed or blocked values."
                    }
                }
                'osVersion' {
                    if (-not $compatibilityCheck.PSObject.Properties['operator'] -or [string]::IsNullOrWhiteSpace([string]$compatibilityCheck.operator)) {
                        throw "Package release '$($release.id)' in '$($definition.id)' has an osVersion compatibility check without operator."
                    }
                    if (-not $compatibilityCheck.PSObject.Properties['value'] -or [string]::IsNullOrWhiteSpace([string]$compatibilityCheck.value)) {
                        throw "Package release '$($release.id)' in '$($definition.id)' has an osVersion compatibility check without value."
                    }
                }
                'physicalMemoryGiB' {
                    if (-not $compatibilityCheck.PSObject.Properties['operator'] -or [string]::IsNullOrWhiteSpace([string]$compatibilityCheck.operator)) {
                        throw "Package release '$($release.id)' in '$($definition.id)' has a physicalMemoryGiB compatibility check without operator."
                    }
                    if (-not $compatibilityCheck.PSObject.Properties['value']) {
                        throw "Package release '$($release.id)' in '$($definition.id)' has a physicalMemoryGiB compatibility check without value."
                    }
                    $parsedValue = 0.0
                    if (-not [double]::TryParse(([string]$compatibilityCheck.value), [System.Globalization.NumberStyles]::Float, [System.Globalization.CultureInfo]::InvariantCulture, [ref]$parsedValue)) {
                        throw "Package release '$($release.id)' in '$($definition.id)' has a physicalMemoryGiB compatibility check with non-numeric value '$($compatibilityCheck.value)'."
                    }
                }
                'videoMemoryGiB' {
                    if (-not $compatibilityCheck.PSObject.Properties['operator'] -or [string]::IsNullOrWhiteSpace([string]$compatibilityCheck.operator)) {
                        throw "Package release '$($release.id)' in '$($definition.id)' has a videoMemoryGiB compatibility check without operator."
                    }
                    if (-not $compatibilityCheck.PSObject.Properties['value']) {
                        throw "Package release '$($release.id)' in '$($definition.id)' has a videoMemoryGiB compatibility check without value."
                    }
                    $parsedValue = 0.0
                    if (-not [double]::TryParse(([string]$compatibilityCheck.value), [System.Globalization.NumberStyles]::Float, [System.Globalization.CultureInfo]::InvariantCulture, [ref]$parsedValue)) {
                        throw "Package release '$($release.id)' in '$($definition.id)' has a videoMemoryGiB compatibility check with non-numeric value '$($compatibilityCheck.value)'."
                    }
                }
                'physicalOrVideoMemoryGiB' {
                    if (-not $compatibilityCheck.PSObject.Properties['operator'] -or [string]::IsNullOrWhiteSpace([string]$compatibilityCheck.operator)) {
                        throw "Package release '$($release.id)' in '$($definition.id)' has a physicalOrVideoMemoryGiB compatibility check without operator."
                    }
                    if (-not $compatibilityCheck.PSObject.Properties['value']) {
                        throw "Package release '$($release.id)' in '$($definition.id)' has a physicalOrVideoMemoryGiB compatibility check without value."
                    }
                    $parsedValue = 0.0
                    if (-not [double]::TryParse(([string]$compatibilityCheck.value), [System.Globalization.NumberStyles]::Float, [System.Globalization.CultureInfo]::InvariantCulture, [ref]$parsedValue)) {
                        throw "Package release '$($release.id)' in '$($definition.id)' has a physicalOrVideoMemoryGiB compatibility check with non-numeric value '$($compatibilityCheck.value)'."
                    }
                }
                default {
                    throw "Package release '$($release.id)' in '$($definition.id)' uses unsupported compatibility kind '$($compatibilityCheck.kind)'."
                }
            }
        }
        if ($effectiveRelease.existingInstallPolicy -and $effectiveRelease.existingInstallPolicy.PSObject.Properties['requireManagedOwnership']) {
            throw "Package release '$($release.id)' in '$($definition.id)' still uses retired property 'requireManagedOwnership'. Use 'requirePackageOwnership'."
        }

        $installKind = if ($effectiveRelease.install -and $effectiveRelease.install.PSObject.Properties['kind']) {
            [string]$effectiveRelease.install.kind
        }
        else {
            $null
        }

        if ([string]::IsNullOrWhiteSpace($installKind)) {
            throw "Package release '$($release.id)' in '$($definition.id)' is missing install.kind."
        }

        if ($installKind -notin @('expandArchive', 'placePackageFile', 'runInstaller', 'nsisInstaller', 'npmGlobalPackage', 'reuseExisting')) {
            throw "Package release '$($release.id)' in '$($definition.id)' uses unsupported install.kind '$installKind'."
        }

        foreach ($retiredInstallProperty in @('managerKind', 'managerDependency')) {
            if ($effectiveRelease.install.PSObject.Properties[$retiredInstallProperty]) {
                throw "Package release '$($release.id)' in '$($definition.id)' still uses retired property 'install.$retiredInstallProperty'. Use install.kind 'npmGlobalPackage' with install.installerCommand."
            }
        }

        if ($effectiveRelease.install.PSObject.Properties['targetKind'] -and
            -not [string]::IsNullOrWhiteSpace([string]$effectiveRelease.install.targetKind) -and
            ([string]$effectiveRelease.install.targetKind) -notin @('directory', 'machinePrerequisite')) {
            throw "Package release '$($release.id)' in '$($definition.id)' uses unsupported install.targetKind '$($effectiveRelease.install.targetKind)'."
        }

        if ($effectiveRelease.install.PSObject.Properties['elevation'] -and
            -not [string]::IsNullOrWhiteSpace([string]$effectiveRelease.install.elevation) -and
            ([string]$effectiveRelease.install.elevation) -notin @('none', 'required', 'auto')) {
            throw "Package release '$($release.id)' in '$($definition.id)' uses unsupported install.elevation '$($effectiveRelease.install.elevation)'."
        }

        if ([string]::Equals($installKind, 'npmGlobalPackage', [System.StringComparison]::OrdinalIgnoreCase)) {
            if (-not $effectiveRelease.install.PSObject.Properties['packageSpec'] -or [string]::IsNullOrWhiteSpace([string]$effectiveRelease.install.packageSpec)) {
                throw "Package release '$($release.id)' in '$($definition.id)' uses install.kind 'npmGlobalPackage' without install.packageSpec."
            }
            if (-not $effectiveRelease.install.PSObject.Properties['installerCommand'] -or [string]::IsNullOrWhiteSpace([string]$effectiveRelease.install.installerCommand)) {
                throw "Package release '$($release.id)' in '$($definition.id)' uses install.kind 'npmGlobalPackage' without install.installerCommand."
            }
        }

        if ([string]::Equals($installKind, 'nsisInstaller', [System.StringComparison]::OrdinalIgnoreCase)) {
            if ($effectiveRelease.install.PSObject.Properties['targetDirectoryArgument'] -and $null -ne $effectiveRelease.install.targetDirectoryArgument) {
                $targetDirectoryArgument = $effectiveRelease.install.targetDirectoryArgument
                if ($targetDirectoryArgument.PSObject.Properties['prefix'] -and [string]::IsNullOrWhiteSpace([string]$targetDirectoryArgument.prefix)) {
                    throw "Package release '$($release.id)' in '$($definition.id)' defines install.targetDirectoryArgument.prefix without a value."
                }
            }
        }

        if ($effectiveRelease.install -and $effectiveRelease.install.PSObject.Properties['pathRegistration'] -and $null -ne $effectiveRelease.install.pathRegistration) {
            $pathRegistration = $effectiveRelease.install.pathRegistration
            if (-not $pathRegistration.PSObject.Properties['mode'] -or [string]::IsNullOrWhiteSpace([string]$pathRegistration.mode)) {
                throw "Package release '$($release.id)' in '$($definition.id)' defines install.pathRegistration without mode."
            }

            $pathRegistrationMode = ([string]$pathRegistration.mode).ToLowerInvariant()
            if ($pathRegistrationMode -notin @('none', 'user', 'machine')) {
                throw "Package release '$($release.id)' in '$($definition.id)' uses unsupported install.pathRegistration.mode '$($pathRegistration.mode)'."
            }

            if ($pathRegistrationMode -ne 'none') {
                if (-not $pathRegistration.PSObject.Properties['source'] -or $null -eq $pathRegistration.source) {
                    throw "Package release '$($release.id)' in '$($definition.id)' defines install.pathRegistration.mode '$($pathRegistration.mode)' without source."
                }
                if (-not $pathRegistration.source.PSObject.Properties['kind'] -or [string]::IsNullOrWhiteSpace([string]$pathRegistration.source.kind)) {
                    throw "Package release '$($release.id)' in '$($definition.id)' defines install.pathRegistration without source.kind."
                }

                switch -Exact ([string]$pathRegistration.source.kind) {
                    'commandEntryPoint' {
                        if (-not $pathRegistration.source.PSObject.Properties['value'] -or [string]::IsNullOrWhiteSpace([string]$pathRegistration.source.value)) {
                            throw "Package release '$($release.id)' in '$($definition.id)' defines install.pathRegistration source kind 'commandEntryPoint' without source.value."
                        }
                        if ($pathRegistration.source.PSObject.Properties['values']) {
                            throw "Package release '$($release.id)' in '$($definition.id)' defines install.pathRegistration source.values for source kind 'commandEntryPoint'."
                        }
                    }
                    'appEntryPoint' {
                        if (-not $pathRegistration.source.PSObject.Properties['value'] -or [string]::IsNullOrWhiteSpace([string]$pathRegistration.source.value)) {
                            throw "Package release '$($release.id)' in '$($definition.id)' defines install.pathRegistration source kind 'appEntryPoint' without source.value."
                        }
                        if ($pathRegistration.source.PSObject.Properties['values']) {
                            throw "Package release '$($release.id)' in '$($definition.id)' defines install.pathRegistration source.values for source kind 'appEntryPoint'."
                        }
                    }
                    'installRelativeDirectory' {
                        if (-not $pathRegistration.source.PSObject.Properties['value'] -or [string]::IsNullOrWhiteSpace([string]$pathRegistration.source.value)) {
                            throw "Package release '$($release.id)' in '$($definition.id)' defines install.pathRegistration source kind 'installRelativeDirectory' without source.value."
                        }
                        if ($pathRegistration.source.PSObject.Properties['values']) {
                            throw "Package release '$($release.id)' in '$($definition.id)' defines install.pathRegistration source.values for source kind 'installRelativeDirectory'."
                        }
                    }
                    'shim' {
                        $hasShimValue = $pathRegistration.source.PSObject.Properties['value'] -and -not [string]::IsNullOrWhiteSpace([string]$pathRegistration.source.value)
                        $hasShimValues = $false
                        if ($pathRegistration.source.PSObject.Properties['values'] -and $null -ne $pathRegistration.source.values) {
                            $hasShimValues = @($pathRegistration.source.values | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) }).Count -gt 0
                        }
                        if (-not $hasShimValue -and -not $hasShimValues) {
                            throw "Package release '$($release.id)' in '$($definition.id)' defines install.pathRegistration source kind 'shim' without source.value or source.values."
                        }
                    }
                    default {
                        throw "Package release '$($release.id)' in '$($definition.id)' uses unsupported install.pathRegistration.source.kind '$($pathRegistration.source.kind)'."
                    }
                }
            }
        }

        $requiresPackageFile = $false
        $requiresAcquisitionCandidates = $false
        switch -Exact ($installKind) {
            'expandArchive' {
                $requiresPackageFile = $true
                $requiresAcquisitionCandidates = $true
            }
            'placePackageFile' {
                $requiresPackageFile = $true
                $requiresAcquisitionCandidates = $true
                if ($effectiveRelease.install.PSObject.Properties['targetRelativePath'] -and
                    [string]::IsNullOrWhiteSpace([string]$effectiveRelease.install.targetRelativePath)) {
                    throw "Package release '$($release.id)' in '$($definition.id)' defines install.targetRelativePath without a value."
                }
            }
            'runInstaller' {
                if (-not $effectiveRelease.install.PSObject.Properties['commandPath'] -or [string]::IsNullOrWhiteSpace([string]$effectiveRelease.install.commandPath)) {
                    $requiresPackageFile = $true
                    $requiresAcquisitionCandidates = $true
                }
            }
            'nsisInstaller' {
                $requiresPackageFile = $true
                $requiresAcquisitionCandidates = $true
            }
        }

        if ($requiresPackageFile) {
            if (-not $effectiveRelease.PSObject.Properties['packageFile'] -or $null -eq $effectiveRelease.packageFile) {
                throw "Package release '$($release.id)' in '$($definition.id)' is missing required property 'packageFile'."
            }
            if (-not $effectiveRelease.packageFile.PSObject.Properties['fileName'] -or [string]::IsNullOrWhiteSpace([string]$effectiveRelease.packageFile.fileName)) {
                throw "Package release '$($release.id)' in '$($definition.id)' is missing packageFile.fileName."
            }
        }

        if ($effectiveRelease.PSObject.Properties['packageFile'] -and $effectiveRelease.packageFile -and
            (-not $effectiveRelease.packageFile.PSObject.Properties['fileName'] -or [string]::IsNullOrWhiteSpace([string]$effectiveRelease.packageFile.fileName))) {
            throw "Package release '$($release.id)' in '$($definition.id)' defines packageFile without packageFile.fileName."
        }
        if ($effectiveRelease.PSObject.Properties['packageFile'] -and $effectiveRelease.packageFile) {
            if ($effectiveRelease.packageFile.PSObject.Properties['autoUpdateSupported']) {
                throw "Package release '$($release.id)' in '$($definition.id)' still uses retired property 'packageFile.autoUpdateSupported'."
            }
            if ($effectiveRelease.packageFile.PSObject.Properties['integrity']) {
                throw "Package release '$($release.id)' in '$($definition.id)' still uses retired property 'packageFile.integrity'. Use 'packageFile.contentHash'."
            }
            if ($effectiveRelease.packageFile.PSObject.Properties['authenticode']) {
                throw "Package release '$($release.id)' in '$($definition.id)' still uses retired property 'packageFile.authenticode'. Use 'packageFile.publisherSignature'."
            }
        }

        if ($effectiveRelease.PSObject.Properties['packageFile'] -and
            $effectiveRelease.packageFile -and
            $effectiveRelease.packageFile.PSObject.Properties['contentHash'] -and
            $null -ne $effectiveRelease.packageFile.contentHash) {
            $contentHash = $effectiveRelease.packageFile.contentHash
            if (-not $contentHash.PSObject.Properties['algorithm'] -or [string]::IsNullOrWhiteSpace([string]$contentHash.algorithm)) {
                throw "Package release '$($release.id)' in '$($definition.id)' defines packageFile.contentHash without algorithm."
            }
            if (-not $contentHash.PSObject.Properties['value'] -or [string]::IsNullOrWhiteSpace([string]$contentHash.value)) {
                throw "Package release '$($release.id)' in '$($definition.id)' defines packageFile.contentHash without value."
            }
            if (-not [string]::Equals([string]$contentHash.algorithm, 'sha256', [System.StringComparison]::OrdinalIgnoreCase)) {
                throw "Package release '$($release.id)' in '$($definition.id)' uses unsupported packageFile.contentHash.algorithm '$($contentHash.algorithm)'."
            }
        }

        if ($effectiveRelease.PSObject.Properties['packageFile'] -and
            $effectiveRelease.packageFile -and
            $effectiveRelease.packageFile.PSObject.Properties['publisherSignature'] -and
            $null -ne $effectiveRelease.packageFile.publisherSignature) {
            $publisherSignature = $effectiveRelease.packageFile.publisherSignature
            if (-not $publisherSignature.PSObject.Properties['kind'] -or [string]::IsNullOrWhiteSpace([string]$publisherSignature.kind)) {
                throw "Package release '$($release.id)' in '$($definition.id)' defines packageFile.publisherSignature without kind."
            }
            if (-not [string]::Equals([string]$publisherSignature.kind, 'authenticode', [System.StringComparison]::OrdinalIgnoreCase)) {
                throw "Package release '$($release.id)' in '$($definition.id)' uses unsupported packageFile.publisherSignature.kind '$($publisherSignature.kind)'."
            }
            if ($publisherSignature.PSObject.Properties['requireValid'] -and
                $null -eq $publisherSignature.requireValid) {
                throw "Package release '$($release.id)' in '$($definition.id)' defines packageFile.publisherSignature.requireValid without a value."
            }
            if ($publisherSignature.PSObject.Properties['subjectContains'] -and
                [string]::IsNullOrWhiteSpace([string]$publisherSignature.subjectContains)) {
                throw "Package release '$($release.id)' in '$($definition.id)' defines packageFile.publisherSignature.subjectContains without a value."
            }
        }

        if ($requiresAcquisitionCandidates) {
            if (-not $effectiveRelease.PSObject.Properties['acquisitionCandidates'] -or @($effectiveRelease.acquisitionCandidates).Count -eq 0) {
                throw "Package release '$($release.id)' in '$($definition.id)' is missing required property 'acquisitionCandidates'."
            }
        }

        if ($effectiveRelease.PSObject.Properties['acquisitionCandidates']) {
            foreach ($candidate in @($effectiveRelease.acquisitionCandidates)) {
                if ($null -eq $candidate) {
                    continue
                }
                if ($candidate.PSObject.Properties['sourceBindingId']) {
                    throw "Package release '$($release.id)' in '$($definition.id)' still uses retired property 'sourceBindingId'."
                }
                if ($candidate.PSObject.Properties['sourceRef']) {
                    throw "Package release '$($release.id)' in '$($definition.id)' still uses retired property 'sourceRef'."
                }
                if ($candidate.PSObject.Properties['priority']) {
                    throw "Package release '$($release.id)' in '$($definition.id)' acquisition candidate still uses retired property 'priority'. Use 'searchOrder'."
                }
                if (-not $candidate.PSObject.Properties['searchOrder']) {
                    throw "Package release '$($release.id)' in '$($definition.id)' has an acquisition candidate without searchOrder."
                }
                if (-not $candidate.PSObject.Properties['kind'] -or [string]::IsNullOrWhiteSpace([string]$candidate.kind)) {
                    throw "Package release '$($release.id)' in '$($definition.id)' has an acquisition candidate without kind."
                }
                switch -Exact ([string]$candidate.kind) {
                    'packageDepot' { }
                    'download' {
                        if (-not $candidate.PSObject.Properties['sourceId'] -or [string]::IsNullOrWhiteSpace([string]$candidate.sourceId)) {
                            throw "Package release '$($release.id)' in '$($definition.id)' has a download acquisition candidate without sourceId."
                        }

                        $downloadSource = $null
                        foreach ($upstreamSourceProperty in @($definition.upstreamSources.PSObject.Properties)) {
                            if ([string]::Equals([string]$upstreamSourceProperty.Name, [string]$candidate.sourceId, [System.StringComparison]::OrdinalIgnoreCase)) {
                                $downloadSource = $upstreamSourceProperty.Value
                                break
                            }
                        }
                        if (-not $downloadSource) {
                            throw "Package release '$($release.id)' in '$($definition.id)' references unknown download sourceId '$($candidate.sourceId)'."
                        }

                        $downloadSourceKind = if ($downloadSource.PSObject.Properties['kind']) { [string]$downloadSource.kind } else { $null }
                        switch -Exact ($downloadSourceKind) {
                            'download' {
                                if (-not $candidate.PSObject.Properties['sourcePath'] -or [string]::IsNullOrWhiteSpace([string]$candidate.sourcePath)) {
                                    throw "Package release '$($release.id)' in '$($definition.id)' has a download acquisition candidate without sourcePath."
                                }
                            }
                            'githubRelease' {
                                if ($candidate.PSObject.Properties['sourcePath'] -and -not [string]::IsNullOrWhiteSpace([string]$candidate.sourcePath)) {
                                    throw "Package release '$($release.id)' in '$($definition.id)' must not define sourcePath for GitHub release source '$($candidate.sourceId)'."
                                }
                                if (-not $effectiveRelease.PSObject.Properties['releaseTag'] -or [string]::IsNullOrWhiteSpace([string]$effectiveRelease.releaseTag)) {
                                    throw "Package release '$($release.id)' in '$($definition.id)' requires releaseTag when download source '$($candidate.sourceId)' is a GitHub release source."
                                }
                                if (-not $effectiveRelease.PSObject.Properties['packageFile'] -or
                                    $null -eq $effectiveRelease.packageFile -or
                                    -not $effectiveRelease.packageFile.PSObject.Properties['fileName'] -or
                                    [string]::IsNullOrWhiteSpace([string]$effectiveRelease.packageFile.fileName)) {
                                    throw "Package release '$($release.id)' in '$($definition.id)' requires packageFile.fileName when download source '$($candidate.sourceId)' is a GitHub release source."
                                }
                            }
                            default {
                                throw "Package release '$($release.id)' in '$($definition.id)' references unsupported download source kind '$downloadSourceKind' for sourceId '$($candidate.sourceId)'."
                            }
                        }
                    }
                    'filesystem' {
                        if (-not $candidate.PSObject.Properties['sourcePath'] -or [string]::IsNullOrWhiteSpace([string]$candidate.sourcePath)) {
                            throw "Package release '$($release.id)' in '$($definition.id)' has a filesystem acquisition candidate without sourcePath."
                        }
                    }
                    default {
                        throw "Package release '$($release.id)' in '$($definition.id)' uses unsupported acquisition kind '$($candidate.kind)'."
                    }
                }
            }
        }

        $existingInstallDiscovery = $effectiveRelease.existingInstallDiscovery
        if ($existingInstallDiscovery.PSObject.Properties['enableDetection'] -and [bool]$existingInstallDiscovery.enableDetection) {
            if (-not $existingInstallDiscovery.PSObject.Properties['searchLocations']) {
                throw "Package release '$($release.id)' in '$($definition.id)' is missing existingInstallDiscovery.searchLocations."
            }
            if (-not $existingInstallDiscovery.PSObject.Properties['installRootRules']) {
                throw "Package release '$($release.id)' in '$($definition.id)' is missing existingInstallDiscovery.installRootRules."
            }
            foreach ($searchLocation in @($existingInstallDiscovery.searchLocations)) {
                if ($null -eq $searchLocation) {
                    continue
                }
                if (-not $searchLocation.PSObject.Properties['kind'] -or [string]::IsNullOrWhiteSpace([string]$searchLocation.kind)) {
                    throw "Package release '$($release.id)' in '$($definition.id)' has an existingInstallDiscovery.searchLocation without kind."
                }
                switch -Exact ([string]$searchLocation.kind) {
                    'command' {
                        if (-not $searchLocation.PSObject.Properties['name'] -or [string]::IsNullOrWhiteSpace([string]$searchLocation.name)) {
                            throw "Package release '$($release.id)' in '$($definition.id)' has a command searchLocation without name."
                        }
                    }
                    'path' {
                        if (-not $searchLocation.PSObject.Properties['path'] -or [string]::IsNullOrWhiteSpace([string]$searchLocation.path)) {
                            throw "Package release '$($release.id)' in '$($definition.id)' has a path searchLocation without path."
                        }
                    }
                    'directory' {
                        if (-not $searchLocation.PSObject.Properties['path'] -or [string]::IsNullOrWhiteSpace([string]$searchLocation.path)) {
                            throw "Package release '$($release.id)' in '$($definition.id)' has a directory searchLocation without path."
                        }
                    }
                    'windowsUninstallRegistryKey' {
                        if (-not $searchLocation.PSObject.Properties['paths'] -or @($searchLocation.paths).Count -eq 0) {
                            throw "Package release '$($release.id)' in '$($definition.id)' has a windowsUninstallRegistryKey searchLocation without paths."
                        }
                        if (-not $searchLocation.PSObject.Properties['installDirectorySource'] -or [string]::IsNullOrWhiteSpace([string]$searchLocation.installDirectorySource)) {
                            throw "Package release '$($release.id)' in '$($definition.id)' has a windowsUninstallRegistryKey searchLocation without installDirectorySource."
                        }
                        if ([string]$searchLocation.installDirectorySource -notin @('installLocation', 'displayIconDirectory', 'uninstallStringDirectory')) {
                            throw "Package release '$($release.id)' in '$($definition.id)' uses unsupported windowsUninstallRegistryKey installDirectorySource '$($searchLocation.installDirectorySource)'."
                        }
                    }
                    default {
                        throw "Package release '$($release.id)' in '$($definition.id)' uses unsupported existingInstallDiscovery.searchLocation kind '$($searchLocation.kind)'."
                    }
                }
            }
            foreach ($rule in @($existingInstallDiscovery.installRootRules)) {
                if ($null -eq $rule) {
                    continue
                }
                if ($rule.PSObject.Properties['fileName'] -or $rule.PSObject.Properties['homePath']) {
                    throw "Package release '$($release.id)' in '$($definition.id)' still uses retired installRootRules fields from installHomeRules."
                }
                if (-not $rule.PSObject.Properties['match'] -or $null -eq $rule.match) {
                    throw "Package release '$($release.id)' in '$($definition.id)' has an installRootRule without match."
                }
                if (-not $rule.match.PSObject.Properties['kind'] -or [string]::IsNullOrWhiteSpace([string]$rule.match.kind)) {
                    throw "Package release '$($release.id)' in '$($definition.id)' has an installRootRule without match.kind."
                }
                if (-not $rule.match.PSObject.Properties['value'] -or [string]::IsNullOrWhiteSpace([string]$rule.match.value)) {
                    throw "Package release '$($release.id)' in '$($definition.id)' has an installRootRule without match.value."
                }
                if (-not $rule.PSObject.Properties['installRootRelativePath']) {
                    throw "Package release '$($release.id)' in '$($definition.id)' has an installRootRule without installRootRelativePath."
                }
            }
        }
    }
}

function Resolve-PackageEffectiveRelease {
<#
.SYNOPSIS
Builds the effective Package release by applying definition defaults.
 
.DESCRIPTION
Applies whole-block fallback from the definition releaseDefaults block to a
single release entry. When a release defines one of the known release blocks,
that block fully replaces the default block.
 
.PARAMETER Definition
The Package definition object.
 
.PARAMETER Release
The raw release object from the definition.
 
.EXAMPLE
Resolve-PackageEffectiveRelease -Definition $definition -Release $release
#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$Definition,

        [Parameter(Mandatory = $true)]
        [psobject]$Release
    )

    $effectiveRelease = ConvertTo-PackageObject -InputObject $Release
    foreach ($propertyName in @('compatibility', 'install', 'validation', 'existingInstallDiscovery', 'existingInstallPolicy')) {
        if (-not $effectiveRelease.PSObject.Properties[$propertyName] -and $Definition.releaseDefaults.PSObject.Properties[$propertyName]) {
            $effectiveRelease | Add-Member -MemberType NoteProperty -Name $propertyName -Value (ConvertTo-PackageObject -InputObject $Definition.releaseDefaults.$propertyName)
        }
    }

    return $effectiveRelease
}