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

<#
    Eigenverft.Manifested.Sandbox.Package.DefinitionSchema.Wire1_1
    Validators for the mandatory baseline wire model (schemaVersion 1.1).
    Isolated from dispatch (DefinitionSchema.ps1) and release merge (ReleaseMerge.ps1).
 
    Validators here run on wire documents only: use shared.discovery / shared.ownershipPolicy (and the
    same names on a release row if present). Never require existingInstallDiscovery / existingInstallPolicy
    in definition JSON — those names exist only on the effective release after Resolve-PackageEffectiveRelease.
#>


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

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

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

    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.PSObject.Properties['releaseDefaults']) {
        throw "Package definition '$($definition.id)' still uses retired property 'releaseDefaults'. Use 'shared' (mandatory baseline wire / schemaVersion 1.1)."
    }
    if (-not $definition.PSObject.Properties['shared'] -or $null -eq $definition.shared) {
        throw "Package definition '$($definition.id)' is missing required property 'shared'."
    }

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

    foreach ($requiredSharedProperty in @('compatibility', 'discovery', 'ownershipPolicy', 'install', 'remove', 'validation')) {
        if (-not $shared.PSObject.Properties[$requiredSharedProperty]) {
            throw "Package definition '$($definition.id)' is missing shared.$requiredSharedProperty."
        }
    }
    foreach ($retiredSharedProperty in @('existingInstall', 'existingInstallDiscovery', 'existingInstallPolicy')) {
        if ($shared.PSObject.Properties[$retiredSharedProperty]) {
            throw "Package definition '$($definition.id)' still uses retired property 'shared.$retiredSharedProperty'. Use 'shared.discovery' and 'shared.ownershipPolicy'."
        }
    }

    $remove = $shared.remove
    foreach ($requiredRemoveProperty in @('keepInstallDirectory', 'keepInventoryRecord', 'keepShims', 'requireProcessExit')) {
        if (-not $remove.PSObject.Properties[$requiredRemoveProperty]) {
            throw "Package definition '$($definition.id)' is missing shared.remove.$requiredRemoveProperty."
        }
    }
}

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

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

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

    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."
            }
        }
    }
}

# schemaVersion '1.1' is the mandatory baseline wire literal (see eigenverft-module-package-definition-1.1.schema.json).
function Assert-PackageDefinitionSchema_1_1 {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$DefinitionDocumentInfo,

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

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

    $definition = $DefinitionDocumentInfo.Document
    Assert-PackageDefinitionWire_DefinitionCore -Definition $definition -DefinitionId $DefinitionId -DefinitionRepositoryId $DefinitionRepositoryId
    foreach ($release in @($definition.releases)) {
        Assert-PackageDefinitionWire_OneRelease -Definition $definition -Release $release -DefinitionRepositoryId $DefinitionRepositoryId
    }
}