Support/Package/Schema/Eigenverft.Manifested.Package.Package.DefinitionCatalogValidation.ps1

<#
    Eigenverft.Manifested.Package.Package.DefinitionCatalogValidation
    Read-only package-definition catalog validation helpers.
#>


function New-PackageDefinitionCatalogValidationIssue {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateSet('Error', 'Warning')]
        [string]$Severity,

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

        [AllowNull()]
        [string]$Path = $null,

        [AllowNull()]
        [string]$PublisherId = $null,

        [AllowNull()]
        [string]$DefinitionId = $null,

        [AllowNull()]
        [string]$JsonPath = $null,

        [AllowNull()]
        [string]$Concept = $null,

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

        [AllowNull()]
        [string]$SuggestedFix = $null
    )

    return [pscustomobject]@{
        Severity     = $Severity
        Code         = $Code
        Path         = $Path
        PublisherId  = $PublisherId
        DefinitionId = $DefinitionId
        JsonPath     = $JsonPath
        Concept      = $Concept
        Message      = $Message
        SuggestedFix = $SuggestedFix
    }
}

function Add-PackageDefinitionCatalogValidationIssue {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [AllowEmptyCollection()]
        [System.Collections.Generic.List[object]]$Issues,

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

    $Issues.Add($Issue) | Out-Null
    return $Issue
}

function ConvertTo-PackageDefinitionCatalogIdentityKey {
    [CmdletBinding()]
    param(
        [AllowNull()]
        [string]$PublisherId = $null,

        [AllowNull()]
        [string]$DefinitionId = $null
    )

    if ([string]::IsNullOrWhiteSpace($PublisherId) -or [string]::IsNullOrWhiteSpace($DefinitionId)) {
        return $null
    }

    return ('{0}|{1}' -f ([string]$PublisherId).Trim().ToUpperInvariant(), ([string]$DefinitionId).Trim().ToUpperInvariant())
}

function Get-PackageDefinitionCatalogValidationDisplayPath {
    [CmdletBinding()]
    param(
        [AllowNull()]
        [string]$Path = $null
    )

    if ([string]::IsNullOrWhiteSpace($Path)) {
        return $Path
    }

    try {
        return [System.IO.Path]::GetFullPath($Path)
    }
    catch {
        return $Path
    }
}

function Get-PackageDefinitionCatalogValidationPathSet {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Path
    )

    $issues = New-Object 'System.Collections.Generic.List[object]'
    $displayPath = Get-PackageDefinitionCatalogValidationDisplayPath -Path $Path
    try {
        $resolvedPath = (Resolve-Path -LiteralPath $Path -ErrorAction Stop).Path
    }
    catch {
        Add-PackageDefinitionCatalogValidationIssue -Issues $issues -Issue (New-PackageDefinitionCatalogValidationIssue -Severity Error -Code CatalogNoJsonFiles -Path $displayPath -Concept 'catalog.path' -Message ("Package-definition catalog path '{0}' could not be resolved." -f $displayPath) -SuggestedFix 'Provide an existing package-definition JSON file or endpoint folder path.') | Out-Null
        return [pscustomobject]@{
            Path      = $displayPath
            Kind      = 'Missing'
            JsonPaths = @()
            Issues    = @($issues.ToArray())
        }
    }

    $kind = if (Test-Path -LiteralPath $resolvedPath -PathType Leaf) { 'File' } else { 'Directory' }
    $jsonPaths = if ([string]::Equals($kind, 'File', [System.StringComparison]::OrdinalIgnoreCase)) {
        @($resolvedPath)
    }
    else {
        @(Get-PackageDefinitionJsonPathsUnderDirectory -DirectoryPath $resolvedPath)
    }

    if ($jsonPaths.Count -eq 0) {
        Add-PackageDefinitionCatalogValidationIssue -Issues $issues -Issue (New-PackageDefinitionCatalogValidationIssue -Severity Error -Code CatalogNoJsonFiles -Path $resolvedPath -Concept 'catalog.files' -Message ("Package-definition catalog path '{0}' does not contain any JSON files." -f $resolvedPath) -SuggestedFix 'Point the command at a package-definition JSON file or an endpoint folder containing JSON package definitions.') | Out-Null
    }

    return [pscustomobject]@{
        Path      = $resolvedPath
        Kind      = $kind
        JsonPaths = @($jsonPaths)
        Issues    = @($issues.ToArray())
    }
}

function New-PackageDefinitionCatalogValidationEntry {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Path
    )

    return [pscustomobject]@{
        Path             = $Path
        Parsed           = $false
        SchemaValid      = $false
        SchemaVersion    = $null
        PublisherId      = $null
        DefinitionId     = $null
        SignatureStatus  = $null
        SignatureValid   = $false
        SignatureTrusted = $false
        Document         = $null
        Issues           = (New-Object 'System.Collections.Generic.List[object]')
    }
}

function Update-PackageDefinitionCatalogValidationEntryIdentity {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$Entry,

        [AllowNull()]
        [psobject]$Document = $null
    )

    if (-not $Document) {
        return
    }

    if ($Document.PSObject.Properties['schemaVersion']) {
        $Entry.SchemaVersion = [string]$Document.schemaVersion
    }

    if ($Document.PSObject.Properties['definitionPublication'] -and $Document.definitionPublication) {
        $publication = $Document.definitionPublication
        if ($publication.PSObject.Properties['publisherId']) {
            $Entry.PublisherId = [string]$publication.publisherId
        }
        if ($publication.PSObject.Properties['definitionId']) {
            $Entry.DefinitionId = [string]$publication.definitionId
        }
    }
}

function Add-PackageDefinitionCatalogSignatureIssue {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$Entry,

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

        [switch]$RequireTrusted
    )

    $status = [string]$SignatureInfo.Status
    $messageDetail = if ($SignatureInfo.PSObject.Properties['ErrorMessage'] -and -not [string]::IsNullOrWhiteSpace([string]$SignatureInfo.ErrorMessage)) {
        [string]$SignatureInfo.ErrorMessage
    }
    else {
        "Signature status is '$status'."
    }

    if ($status -in @('missingSignature', 'unsigned')) {
        $severity = if ($RequireTrusted.IsPresent) { 'Error' } else { 'Warning' }
        Add-PackageDefinitionCatalogValidationIssue -Issues $Entry.Issues -Issue (New-PackageDefinitionCatalogValidationIssue -Severity $severity -Code PackageDefinitionSignatureUnsigned -Path ([string]$Entry.Path) -PublisherId ([string]$Entry.PublisherId) -DefinitionId ([string]$Entry.DefinitionId) -JsonPath 'definitionPublication.definitionSignature' -Concept 'signature.trust' -Message ("Package definition '{0}' is unsigned or missing a signature." -f [string]$Entry.Path) -SuggestedFix 'Sign the package definition with Sign-PackageDefinition or omit -RequireTrusted for draft validation.') | Out-Null
        return
    }

    if ($status -in @('validUntrusted', 'unknownKey')) {
        $severity = if ($RequireTrusted.IsPresent) { 'Error' } else { 'Warning' }
        Add-PackageDefinitionCatalogValidationIssue -Issues $Entry.Issues -Issue (New-PackageDefinitionCatalogValidationIssue -Severity $severity -Code PackageDefinitionSignatureUntrusted -Path ([string]$Entry.Path) -PublisherId ([string]$Entry.PublisherId) -DefinitionId ([string]$Entry.DefinitionId) -JsonPath 'definitionPublication.definitionSignature' -Concept 'signature.trust' -Message ("Package definition '{0}' signature is not trusted. {1}" -f [string]$Entry.Path, $messageDetail) -SuggestedFix 'Trust the signing certificate, provide -CertificatePath for validation, or omit -RequireTrusted for non-strict validation.') | Out-Null
        return
    }

    if (-not [bool]$SignatureInfo.Valid) {
        Add-PackageDefinitionCatalogValidationIssue -Issues $Entry.Issues -Issue (New-PackageDefinitionCatalogValidationIssue -Severity Error -Code PackageDefinitionSignatureInvalid -Path ([string]$Entry.Path) -PublisherId ([string]$Entry.PublisherId) -DefinitionId ([string]$Entry.DefinitionId) -JsonPath 'definitionPublication.definitionSignature' -Concept 'signature.validity' -Message ("Package definition '{0}' signature is invalid. {1}" -f [string]$Entry.Path, $messageDetail) -SuggestedFix 'Re-sign the package definition after fixing its JSON content, certificate metadata, or signature value.') | Out-Null
        return
    }

    if (-not [bool]$SignatureInfo.Trusted) {
        $severity = if ($RequireTrusted.IsPresent) { 'Error' } else { 'Warning' }
        Add-PackageDefinitionCatalogValidationIssue -Issues $Entry.Issues -Issue (New-PackageDefinitionCatalogValidationIssue -Severity $severity -Code PackageDefinitionSignatureUntrusted -Path ([string]$Entry.Path) -PublisherId ([string]$Entry.PublisherId) -DefinitionId ([string]$Entry.DefinitionId) -JsonPath 'definitionPublication.definitionSignature' -Concept 'signature.trust' -Message ("Package definition '{0}' signature is valid but not trusted." -f [string]$Entry.Path) -SuggestedFix 'Trust the signing certificate or omit -RequireTrusted for non-strict validation.') | Out-Null
    }
}

function Test-PackageDefinitionCatalogDocument {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Path,

        [AllowNull()]
        [System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate = $null,

        [AllowNull()]
        [psobject]$TrustInventoryDocument = $null,

        [switch]$RequireTrusted
    )

    $entry = New-PackageDefinitionCatalogValidationEntry -Path $Path
    try {
        $definitionInfo = Read-PackageJsonDocument -Path $Path
    }
    catch {
        Add-PackageDefinitionCatalogValidationIssue -Issues $entry.Issues -Issue (New-PackageDefinitionCatalogValidationIssue -Severity Error -Code CatalogJsonParseFailed -Path $Path -Concept 'json.parse' -Message $_.Exception.Message -SuggestedFix 'Fix the JSON syntax or file accessibility before validating schema and trust.') | Out-Null
        return $entry
    }

    $entry.Parsed = $true
    $entry.Document = $definitionInfo.Document
    Update-PackageDefinitionCatalogValidationEntryIdentity -Entry $entry -Document $definitionInfo.Document

    $expectedDefinitionId = if ([string]::IsNullOrWhiteSpace([string]$entry.DefinitionId)) { '<unknown>' } else { [string]$entry.DefinitionId }
    $expectedPublisherId = if ([string]::IsNullOrWhiteSpace([string]$entry.PublisherId)) { $null } else { [string]$entry.PublisherId }
    try {
        Assert-PackageDefinitionSchema -DefinitionDocumentInfo $definitionInfo -DefinitionId $expectedDefinitionId -PublisherId $expectedPublisherId
        $entry.SchemaValid = $true
    }
    catch {
        Add-PackageDefinitionCatalogValidationIssue -Issues $entry.Issues -Issue (New-PackageDefinitionCatalogValidationIssue -Severity Error -Code PackageDefinitionSchemaInvalid -Path $Path -PublisherId ([string]$entry.PublisherId) -DefinitionId ([string]$entry.DefinitionId) -Concept 'schema.1.9' -Message $_.Exception.Message -SuggestedFix 'Update the package-definition JSON to satisfy the current schemaVersion 1.9 wire contract.') | Out-Null
    }

    try {
        $signatureInfo = Test-PackageDefinitionSignatureDocument -Definition $definitionInfo.Document -Certificate $Certificate -TrustInventoryDocument $TrustInventoryDocument
        $entry.SignatureStatus = [string]$signatureInfo.Status
        $entry.SignatureValid = [bool]$signatureInfo.Valid
        $entry.SignatureTrusted = [bool]$signatureInfo.Trusted
        Add-PackageDefinitionCatalogSignatureIssue -Entry $entry -SignatureInfo $signatureInfo -RequireTrusted:$RequireTrusted
    }
    catch {
        Add-PackageDefinitionCatalogValidationIssue -Issues $entry.Issues -Issue (New-PackageDefinitionCatalogValidationIssue -Severity Error -Code PackageDefinitionSignatureInvalid -Path $Path -PublisherId ([string]$entry.PublisherId) -DefinitionId ([string]$entry.DefinitionId) -JsonPath 'definitionPublication.definitionSignature' -Concept 'signature.validity' -Message $_.Exception.Message -SuggestedFix 'Fix the definition signature metadata or re-sign the package definition.') | Out-Null
    }

    return $entry
}

function Add-PackageDefinitionCatalogValidationReferenceIssue {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$Entry,

        [Parameter(Mandatory = $true)]
        [ValidateSet('Dependency', 'Policy')]
        [string]$ReferenceKind,

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

        [AllowNull()]
        [string]$ReferencePublisherId = $null,

        [AllowNull()]
        [string]$ReferenceDefinitionId = $null,

        [Parameter(Mandatory = $true)]
        [bool]$SelfReference
    )

    if ([string]::Equals($ReferenceKind, 'Dependency', [System.StringComparison]::OrdinalIgnoreCase)) {
        $code = if ($SelfReference) { 'CatalogDependencySelfReference' } else { 'CatalogDependencyReferenceMissing' }
        $concept = 'dependency.requires'
        $message = if ($SelfReference) {
            "Package definition '$($Entry.DefinitionId)' references itself in dependency.requires."
        }
        else {
            "Package definition '$($Entry.DefinitionId)' references missing dependency '${ReferencePublisherId}:${ReferenceDefinitionId}'."
        }
        $fix = if ($SelfReference) { 'Remove the self dependency from dependency.requires.' } else { 'Add the referenced package definition to the validated endpoint folder or correct the dependency reference.' }
    }
    else {
        $code = if ($SelfReference) { 'CatalogPolicySelfReference' } else { 'CatalogPolicyReferenceMissing' }
        $concept = 'dependency.policy'
        $message = if ($SelfReference) {
            "Package definition '$($Entry.DefinitionId)' references itself in dependency.policy."
        }
        else {
            "Package definition '$($Entry.DefinitionId)' references missing dependency policy target '${ReferencePublisherId}:${ReferenceDefinitionId}'."
        }
        $fix = if ($SelfReference) { 'Remove the self reference from dependency.policy.' } else { 'Add the referenced package definition to the validated endpoint folder or correct the policy reference.' }
    }

    Add-PackageDefinitionCatalogValidationIssue -Issues $Entry.Issues -Issue (New-PackageDefinitionCatalogValidationIssue -Severity Error -Code $code -Path ([string]$Entry.Path) -PublisherId ([string]$Entry.PublisherId) -DefinitionId ([string]$Entry.DefinitionId) -JsonPath $JsonPath -Concept $concept -Message $message -SuggestedFix $fix) | Out-Null
}

function Add-PackageDefinitionCatalogSemanticWarning {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$Entry,

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

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

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

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

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

    Add-PackageDefinitionCatalogValidationIssue -Issues $Entry.Issues -Issue (New-PackageDefinitionCatalogValidationIssue -Severity Warning -Code $Code -Path ([string]$Entry.Path) -PublisherId ([string]$Entry.PublisherId) -DefinitionId ([string]$Entry.DefinitionId) -JsonPath $JsonPath -Concept $Concept -Message $Message -SuggestedFix $SuggestedFix) | Out-Null
}

function Get-PackageDefinitionCatalogRequiredPresenceNames {
    [CmdletBinding()]
    param(
        [AllowNull()]
        [psobject]$Require = $null
    )

    if (-not $Require) {
        return @()
    }

    return @(
        foreach ($name in @('files', 'directories', 'commands', 'apps', 'metadataFiles', 'signatures', 'fileDetails', 'registry', 'powerShellModules')) {
            if ($Require.PSObject.Properties[$name] -and [bool]$Require.$name) {
                $name
            }
        }
    )
}

function Test-PackageDefinitionCatalogSemanticWarnings {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [AllowEmptyCollection()]
        [object[]]$Entries
    )

    $schemaValidEntries = @($Entries | Where-Object { [bool]$_.SchemaValid })
    foreach ($entry in @($schemaValidEntries)) {
        $definition = $entry.Document
        if (-not $definition -or -not $definition.PSObject.Properties['packageOperations']) {
            continue
        }

        $assigned = $definition.packageOperations.assigned
        $install = if ($assigned -and $assigned.PSObject.Properties['install']) { $assigned.install } else { $null }
        if (-not $install) {
            continue
        }

        $installKind = if ($install.PSObject.Properties['kind']) { [string]$install.kind } else { $null }
        $targetKind = if ($install.PSObject.Properties['targetKind'] -and -not [string]::IsNullOrWhiteSpace([string]$install.targetKind)) {
            [string]$install.targetKind
        }
        else {
            'directory'
        }
        $hasInstallDirectory = $install.PSObject.Properties['installDirectory'] -and -not [string]::IsNullOrWhiteSpace([string]$install.installDirectory)
        $isMachinePrerequisite = [string]::Equals($targetKind, 'machinePrerequisite', [System.StringComparison]::OrdinalIgnoreCase)
        $installDirectoryOptionalKinds = @('reuseExisting', 'powershellModuleInstaller')
        $installKindAllowsMissingDirectory = @($installDirectoryOptionalKinds | Where-Object { [string]::Equals($_, $installKind, [System.StringComparison]::OrdinalIgnoreCase) }).Count -gt 0

        if (-not $hasInstallDirectory -and -not $isMachinePrerequisite -and -not $installKindAllowsMissingDirectory) {
            Add-PackageDefinitionCatalogSemanticWarning -Entry $entry -Code PackageDefinitionInstallTargetMissing -JsonPath 'packageOperations.assigned.install' -Concept 'install.targetPath' -Message ("Package definition '$($entry.DefinitionId)' uses assigned install kind '$installKind' without installDirectory and without targetKind='machinePrerequisite'. Runtime path resolution usually requires an install target path for this operation.") -SuggestedFix "Add packageOperations.assigned.install.installDirectory, change targetKind to machinePrerequisite only when readiness is install-root-free, or choose a schema operation that does not require a package-owned install directory."
        }

        $argumentsWithInstallDirectory = @(
            foreach ($argument in @($install.commandArguments)) {
                if ($null -ne $argument -and [string]$argument -match '\{installDirectory\}') {
                    [string]$argument
                }
            }
        )
        if ($argumentsWithInstallDirectory.Count -gt 0 -and (-not $hasInstallDirectory -or $isMachinePrerequisite)) {
            Add-PackageDefinitionCatalogSemanticWarning -Entry $entry -Code PackageDefinitionInstallDirectoryArgumentWithoutTarget -JsonPath 'packageOperations.assigned.install.commandArguments' -Concept 'install.arguments.installDirectory' -Message ("Package definition '$($entry.DefinitionId)' passes {installDirectory} in installer arguments, but the install operation does not own an installDirectory usable by that argument.") -SuggestedFix 'Remove the custom install-directory argument, add a real schema installDirectory only for package-managed installs, or stop if the installer-owned default location cannot be represented safely.'
        }

        $readyRequire = if ($assigned.PSObject.Properties['readyStateCheck'] -and $assigned.readyStateCheck.PSObject.Properties['require']) {
            $assigned.readyStateCheck.require
        }
        else {
            $null
        }
        $readyRequired = @(Get-PackageDefinitionCatalogRequiredPresenceNames -Require $readyRequire)
        $installRootReadinessNames = @($readyRequired | Where-Object { $_ -in @('files', 'directories', 'commands', 'apps', 'metadataFiles', 'signatures', 'fileDetails') })
        $installRootFree = $isMachinePrerequisite -or (-not $hasInstallDirectory -and -not $installKindAllowsMissingDirectory)
        if ($installRootFree -and $installRootReadinessNames.Count -gt 0) {
            Add-PackageDefinitionCatalogSemanticWarning -Entry $entry -Code PackageDefinitionInstallRootReadinessWithoutInstallRoot -JsonPath 'packageOperations.assigned.readyStateCheck.require' -Concept 'readiness.installRoot' -Message ("Package definition '$($entry.DefinitionId)' requires install-root readiness checks ($($installRootReadinessNames -join ', ')) but the install operation is install-root-free.") -SuggestedFix 'Use only registry, PowerShell module, or other install-root-free readiness signals, or stop until the schema/runtime can discover the installer-owned install root.'
        }

        $targets = if ($definition.PSObject.Properties['artifacts'] -and $definition.artifacts.PSObject.Properties['targets']) { @($definition.artifacts.targets) } else { @() }
        $cpuValues = @(
            foreach ($target in @($targets)) {
                if ($target.PSObject.Properties['constraints'] -and $target.constraints.PSObject.Properties['cpu']) {
                    foreach ($cpu in @($target.constraints.cpu)) {
                        if (-not [string]::IsNullOrWhiteSpace([string]$cpu)) {
                            [string]$cpu
                        }
                    }
                }
            }
        ) | Sort-Object -Unique
        $architectureSpecificReadinessPaths = @()
        if ($cpuValues.Count -gt 1 -and $installRootReadinessNames.Count -gt 0 -and $definition.PSObject.Properties['discovery'] -and $definition.discovery.PSObject.Properties['presence']) {
            $presence = $definition.discovery.presence
            $readinessPathCandidates = New-Object System.Collections.Generic.List[string]
            foreach ($pathText in @($presence.files) + @($presence.directories)) {
                if (-not [string]::IsNullOrWhiteSpace([string]$pathText)) {
                    $readinessPathCandidates.Add([string]$pathText) | Out-Null
                }
            }
            foreach ($entryPoint in @($presence.commands) + @($presence.apps) + @($presence.signatures) + @($presence.fileDetails) + @($presence.metadataFiles)) {
                if ($entryPoint -and $entryPoint.PSObject.Properties['relativePath'] -and -not [string]::IsNullOrWhiteSpace([string]$entryPoint.relativePath)) {
                    $readinessPathCandidates.Add([string]$entryPoint.relativePath) | Out-Null
                }
            }
            $architectureSpecificReadinessPaths = @(
                $readinessPathCandidates.ToArray() |
                    Where-Object { [regex]::IsMatch([string]$_, '(?i)(x64|x86|amd64|arm64|aarch64|win64|win32|64(?=\.|_|-|$)|32(?=\.|_|-|$))') } |
                    Sort-Object -Unique
            )
        }
        if ($cpuValues.Count -gt 1 -and $architectureSpecificReadinessPaths.Count -gt 0) {
            Add-PackageDefinitionCatalogSemanticWarning -Entry $entry -Code PackageDefinitionSharedReadinessAcrossArchitectures -JsonPath 'discovery.presence' -Concept 'readiness.targetArchitecture' -Message ("Package definition '$($entry.DefinitionId)' has multiple CPU targets ($($cpuValues -join ', ')) but shared readiness contains architecture-specific install-root paths: $($architectureSpecificReadinessPaths -join ', '). A shared readiness model must be valid for every selectable target.") -SuggestedFix 'Use an artifact that satisfies one shared readiness model, make readiness target-independent, or stop for a schema/runtime decision before mixing architecture-specific executable/file expectations.'
        }

        $removed = if ($definition.packageOperations.PSObject.Properties['removed']) { $definition.packageOperations.removed } else { $null }
        if ($removed) {
            $operation = if ($removed.PSObject.Properties['operation']) { $removed.operation } else { $null }
            $operationKind = if ($operation -and $operation.PSObject.Properties['kind']) { [string]$operation.kind } else { $null }
            $absenceRequire = if ($removed.PSObject.Properties['absenceVerification'] -and $removed.absenceVerification.PSObject.Properties['require']) {
                $removed.absenceVerification.require
            }
            else {
                $null
            }
            $absenceRequired = @(Get-PackageDefinitionCatalogRequiredPresenceNames -Require $absenceRequire)
            if ([string]::Equals($operationKind, 'none', [System.StringComparison]::OrdinalIgnoreCase) -and $absenceRequired.Count -gt 0) {
                Add-PackageDefinitionCatalogSemanticWarning -Entry $entry -Code PackageDefinitionNoOpRemovalRequiresAbsence -JsonPath 'packageOperations.removed.absenceVerification.require' -Concept 'removed.noopAbsence' -Message ("Package definition '$($entry.DefinitionId)' uses removed.operation.kind='none' but still requires absence signals after removal: $($absenceRequired -join ', '). A no-op removal cannot make those signals absent.") -SuggestedFix 'Use a schema-supported uninstaller/removal operation, disable absence requirements that the no-op cannot change, or stop if removed state cannot be represented.'
            }
        }

        if ($isMachinePrerequisite -and -not $hasInstallDirectory) {
            Add-PackageDefinitionCatalogSemanticWarning -Entry $entry -Code PackageDefinitionMachinePrerequisiteRemovalInventoryRisk -JsonPath 'packageOperations.assigned.install.targetKind' -Concept 'removed.inventoryInstallDirectory' -Message ("Package definition '$($entry.DefinitionId)' is a machinePrerequisite without installDirectory. Assignment can create a PackageApplied inventory record without installDirectory, while removal flows currently require inventory.installDirectory before executing removed.operation.") -SuggestedFix 'Prove removed-state handling does not require inventory.installDirectory, avoid claiming removed-state support, or add schema/runtime support for machine-prerequisite forget/removal semantics.'
        }
    }
}

function Test-PackageDefinitionCatalogStaticReferences {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [AllowEmptyCollection()]
        [object[]]$Entries
    )

    $schemaValidEntries = @($Entries | Where-Object { [bool]$_.SchemaValid })
    $targetsByKey = @{}
    foreach ($entry in @($schemaValidEntries)) {
        $key = ConvertTo-PackageDefinitionCatalogIdentityKey -PublisherId ([string]$entry.PublisherId) -DefinitionId ([string]$entry.DefinitionId)
        if ($key) {
            $targetsByKey[$key] = $entry
        }
    }

    foreach ($entry in @($schemaValidEntries)) {
        $sourceKey = ConvertTo-PackageDefinitionCatalogIdentityKey -PublisherId ([string]$entry.PublisherId) -DefinitionId ([string]$entry.DefinitionId)
        $dependencyModel = Get-PackageDefinitionDependencyModel_1_9 -Definition $entry.Document -DefinitionId ([string]$entry.DefinitionId)
        $dependencyIndex = 0
        foreach ($dependency in @($dependencyModel.Requires)) {
            if ($null -eq $dependency) {
                $dependencyIndex++
                continue
            }
            $referencePublisherId = if ($dependency.PSObject.Properties['publisherId'] -and -not [string]::IsNullOrWhiteSpace([string]$dependency.publisherId)) { [string]$dependency.publisherId } else { [string]$entry.PublisherId }
            $referenceDefinitionId = if ($dependency.PSObject.Properties['definitionId']) { [string]$dependency.definitionId } else { $null }
            $referenceKey = ConvertTo-PackageDefinitionCatalogIdentityKey -PublisherId $referencePublisherId -DefinitionId $referenceDefinitionId
            $jsonPath = 'dependency.requires[{0}]' -f $dependencyIndex
            if ($referenceKey -and [string]::Equals($referenceKey, $sourceKey, [System.StringComparison]::OrdinalIgnoreCase)) {
                Add-PackageDefinitionCatalogValidationReferenceIssue -Entry $entry -ReferenceKind Dependency -JsonPath $jsonPath -ReferencePublisherId $referencePublisherId -ReferenceDefinitionId $referenceDefinitionId -SelfReference $true
            }
            elseif ($referenceKey -and -not $targetsByKey.ContainsKey($referenceKey)) {
                Add-PackageDefinitionCatalogValidationReferenceIssue -Entry $entry -ReferenceKind Dependency -JsonPath $jsonPath -ReferencePublisherId $referencePublisherId -ReferenceDefinitionId $referenceDefinitionId -SelfReference $false
            }
            $dependencyIndex++
        }

        foreach ($policyPropertyName in @('conflictsWith', 'requiresAbsent')) {
            $policyReferences = if ($dependencyModel.Policy -and $dependencyModel.Policy.PSObject.Properties[$policyPropertyName]) { @($dependencyModel.Policy.$policyPropertyName) } else { @() }
            $policyIndex = 0
            foreach ($policyReference in @($policyReferences)) {
                if ($null -eq $policyReference) {
                    $policyIndex++
                    continue
                }
                $referencePublisherId = if ($policyReference.PSObject.Properties['publisherId'] -and -not [string]::IsNullOrWhiteSpace([string]$policyReference.publisherId)) { [string]$policyReference.publisherId } else { [string]$entry.PublisherId }
                $referenceDefinitionId = if ($policyReference.PSObject.Properties['definitionId']) { [string]$policyReference.definitionId } else { $null }
                $referenceKey = ConvertTo-PackageDefinitionCatalogIdentityKey -PublisherId $referencePublisherId -DefinitionId $referenceDefinitionId
                $jsonPath = 'dependency.policy.{0}[{1}]' -f $policyPropertyName, $policyIndex
                if ($referenceKey -and [string]::Equals($referenceKey, $sourceKey, [System.StringComparison]::OrdinalIgnoreCase)) {
                    Add-PackageDefinitionCatalogValidationReferenceIssue -Entry $entry -ReferenceKind Policy -JsonPath $jsonPath -ReferencePublisherId $referencePublisherId -ReferenceDefinitionId $referenceDefinitionId -SelfReference $true
                }
                elseif ($referenceKey -and -not $targetsByKey.ContainsKey($referenceKey)) {
                    Add-PackageDefinitionCatalogValidationReferenceIssue -Entry $entry -ReferenceKind Policy -JsonPath $jsonPath -ReferencePublisherId $referencePublisherId -ReferenceDefinitionId $referenceDefinitionId -SelfReference $false
                }
                $policyIndex++
            }
        }
    }
}

function Add-PackageDefinitionCatalogDuplicateIdentityIssues {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [AllowEmptyCollection()]
        [object[]]$Entries
    )

    $entriesByKey = @{}
    foreach ($entry in @($Entries | Where-Object { [bool]$_.SchemaValid })) {
        $key = ConvertTo-PackageDefinitionCatalogIdentityKey -PublisherId ([string]$entry.PublisherId) -DefinitionId ([string]$entry.DefinitionId)
        if (-not $key) {
            continue
        }
        if (-not $entriesByKey.ContainsKey($key)) {
            $entriesByKey[$key] = New-Object 'System.Collections.Generic.List[object]'
        }
        $entriesByKey[$key].Add($entry) | Out-Null
    }

    foreach ($key in @($entriesByKey.Keys)) {
        $duplicates = @($entriesByKey[$key].ToArray())
        if ($duplicates.Count -le 1) {
            continue
        }
        $paths = (@($duplicates) | ForEach-Object { [string]$_.Path }) -join ', '
        foreach ($entry in @($duplicates)) {
            Add-PackageDefinitionCatalogValidationIssue -Issues $entry.Issues -Issue (New-PackageDefinitionCatalogValidationIssue -Severity Error -Code CatalogDuplicateDefinitionIdentity -Path ([string]$entry.Path) -PublisherId ([string]$entry.PublisherId) -DefinitionId ([string]$entry.DefinitionId) -JsonPath 'definitionPublication' -Concept 'catalog.identity' -Message ("Package definition identity '{0}:{1}' appears more than once in this catalog: {2}" -f [string]$entry.PublisherId, [string]$entry.DefinitionId, $paths) -SuggestedFix 'Keep only one JSON file per publisherId and definitionId in the validated catalog folder.') | Out-Null
        }
    }
}

function Add-PackageDefinitionCatalogMixedSchemaVersionIssues {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [AllowEmptyCollection()]
        [object[]]$Entries,

        [switch]$StrictSchemaVersion
    )

    $schemaVersions = @($Entries | Where-Object { [bool]$_.Parsed -and -not [string]::IsNullOrWhiteSpace([string]$_.SchemaVersion) } | ForEach-Object { [string]$_.SchemaVersion } | Sort-Object -Unique)
    if ($schemaVersions.Count -le 1) {
        return
    }

    $severity = if ($StrictSchemaVersion.IsPresent) { 'Error' } else { 'Warning' }
    $message = "Package-definition catalog contains mixed schemaVersion values: $($schemaVersions -join ', ')."
    foreach ($entry in @($Entries | Where-Object { [bool]$_.Parsed -and -not [string]::IsNullOrWhiteSpace([string]$_.SchemaVersion) })) {
        Add-PackageDefinitionCatalogValidationIssue -Issues $entry.Issues -Issue (New-PackageDefinitionCatalogValidationIssue -Severity $severity -Code CatalogMixedSchemaVersion -Path ([string]$entry.Path) -PublisherId ([string]$entry.PublisherId) -DefinitionId ([string]$entry.DefinitionId) -JsonPath 'schemaVersion' -Concept 'catalog.schemaVersion' -Message $message -SuggestedFix 'Use one supported schemaVersion across the endpoint folder, currently schemaVersion 1.9.') | Out-Null
    }
}

function New-PackageDefinitionCatalogValidationFileResult {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$Entry
    )

    $issues = @($Entry.Issues.ToArray())
    return [pscustomobject]@{
        Path             = [string]$Entry.Path
        Parsed           = [bool]$Entry.Parsed
        SchemaValid      = [bool]$Entry.SchemaValid
        SchemaVersion    = if ([string]::IsNullOrWhiteSpace([string]$Entry.SchemaVersion)) { $null } else { [string]$Entry.SchemaVersion }
        PublisherId      = if ([string]::IsNullOrWhiteSpace([string]$Entry.PublisherId)) { $null } else { [string]$Entry.PublisherId }
        DefinitionId     = if ([string]::IsNullOrWhiteSpace([string]$Entry.DefinitionId)) { $null } else { [string]$Entry.DefinitionId }
        SignatureStatus  = if ([string]::IsNullOrWhiteSpace([string]$Entry.SignatureStatus)) { $null } else { [string]$Entry.SignatureStatus }
        SignatureValid   = [bool]$Entry.SignatureValid
        SignatureTrusted = [bool]$Entry.SignatureTrusted
        ErrorCount       = @($issues | Where-Object { [string]$_.Severity -eq 'Error' }).Count
        WarningCount     = @($issues | Where-Object { [string]$_.Severity -eq 'Warning' }).Count
        Issues           = @($issues)
    }
}

function Invoke-PackageDefinitionCatalogValidation {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Path,

        [AllowNull()]
        [string]$CertificatePath = $null,

        [switch]$RequireTrusted,

        [switch]$StrictSchemaVersion
    )

    $pathSet = Get-PackageDefinitionCatalogValidationPathSet -Path $Path
    $certificate = $null
    $trustInventoryDocument = $null
    $entries = New-Object 'System.Collections.Generic.List[object]'
    try {
        if (@($pathSet.JsonPaths).Count -gt 0) {
            if (-not [string]::IsNullOrWhiteSpace($CertificatePath)) {
                $certificate = Import-PackageCertificate -Path $CertificatePath
            }
            else {
                $trustInventoryDocument = (Get-PackageTrustInventoryInfo).Document
            }

            foreach ($jsonPath in @($pathSet.JsonPaths)) {
                $entries.Add((Test-PackageDefinitionCatalogDocument -Path $jsonPath -Certificate $certificate -TrustInventoryDocument $trustInventoryDocument -RequireTrusted:$RequireTrusted)) | Out-Null
            }
        }

        if ([string]::Equals([string]$pathSet.Kind, 'Directory', [System.StringComparison]::OrdinalIgnoreCase)) {
            Add-PackageDefinitionCatalogDuplicateIdentityIssues -Entries @($entries.ToArray())
            Add-PackageDefinitionCatalogMixedSchemaVersionIssues -Entries @($entries.ToArray()) -StrictSchemaVersion:$StrictSchemaVersion
            Test-PackageDefinitionCatalogStaticReferences -Entries @($entries.ToArray())
        }
        Test-PackageDefinitionCatalogSemanticWarnings -Entries @($entries.ToArray())
    }
    finally {
        if ($certificate) {
            $certificate.Dispose()
        }
    }

    $results = @(
        foreach ($entry in @($entries.ToArray())) {
            New-PackageDefinitionCatalogValidationFileResult -Entry $entry
        }
    )
    $issues = @(@($pathSet.Issues) + @($results | ForEach-Object { @($_.Issues) }))
    $errorCount = @($issues | Where-Object { [string]$_.Severity -eq 'Error' }).Count
    $warningCount = @($issues | Where-Object { [string]$_.Severity -eq 'Warning' }).Count

    return [pscustomobject]@{
        Path                = [string]$pathSet.Path
        Kind                = [string]$pathSet.Kind
        Valid               = ($errorCount -eq 0)
        CheckedCount        = $results.Count
        ParsedCount         = @($results | Where-Object { [bool]$_.Parsed }).Count
        SchemaValidCount    = @($results | Where-Object { [bool]$_.SchemaValid }).Count
        TrustedCount        = @($results | Where-Object { [bool]$_.SignatureTrusted }).Count
        ErrorCount          = $errorCount
        WarningCount        = $warningCount
        RequireTrusted      = [bool]$RequireTrusted.IsPresent
        StrictSchemaVersion = [bool]$StrictSchemaVersion.IsPresent
        Issues              = @($issues)
        Results             = @($results)
    }
}