Support/Package/Schema/Eigenverft.Manifested.Sandbox.Package.Config.InventoryAndSchema.ps1

<#
    Eigenverft.Manifested.Sandbox.Package.Config — depot/source inventory and global config schema assertions.
    Loaded by Eigenverft.Manifested.Sandbox.Package.Config.ps1.
#>


function Get-PackageSourceInventoryPath {
<#
.SYNOPSIS
Returns the effective external Package source-inventory path.
 
.DESCRIPTION
Resolves the environment-variable override first, then falls back to the
well-known local inventory path.
 
.EXAMPLE
Get-PackageSourceInventoryPath
#>

    [CmdletBinding()]
    param(
        [AllowNull()]
        [string]$ApplicationRootDirectory
    )

    $environmentVariableName = Get-PackageSourceInventoryPathEnvironmentVariableName
    $configuredPath = [Environment]::GetEnvironmentVariable($environmentVariableName)
    if (-not [string]::IsNullOrWhiteSpace($configuredPath)) {
        return (Resolve-PackagePathValue -PathValue $configuredPath)
    }

    if (-not [string]::IsNullOrWhiteSpace($ApplicationRootDirectory)) {
        return (Resolve-PackageConfiguredPath -PathValue 'Configuration\External\SourceInventory.json' -ApplicationRootDirectory $ApplicationRootDirectory)
    }

    return (Get-PackageDefaultSourceInventoryPath)
}

function Get-PackageSiteCode {
<#
.SYNOPSIS
Returns the effective Package site code.
 
.DESCRIPTION
Reads the optional site-code environment variable and normalizes its value.
 
.EXAMPLE
Get-PackageSiteCode
#>

    [CmdletBinding()]
    param()

    $environmentVariableName = Get-PackageSiteCodeEnvironmentVariableName
    $siteCode = [Environment]::GetEnvironmentVariable($environmentVariableName)
    if ([string]::IsNullOrWhiteSpace($siteCode)) {
        return $null
    }

    return $siteCode.Trim()
}

function Get-PackageActiveSiteCodes {
<#
.SYNOPSIS
Returns the effective Package site-code list.
 
.DESCRIPTION
Reads the optional site-code environment variable as a semicolon-separated
list, trims empty entries, and de-duplicates while preserving order.
 
.EXAMPLE
Get-PackageActiveSiteCodes
#>

    [CmdletBinding()]
    param()

    $environmentVariableName = Get-PackageSiteCodeEnvironmentVariableName
    $siteCodeText = [Environment]::GetEnvironmentVariable($environmentVariableName)
    if ([string]::IsNullOrWhiteSpace($siteCodeText)) {
        return @()
    }

    $seen = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
    $siteCodes = New-Object System.Collections.Generic.List[string]
    foreach ($siteCode in @($siteCodeText -split ';')) {
        $normalizedSiteCode = ([string]$siteCode).Trim()
        if ([string]::IsNullOrWhiteSpace($normalizedSiteCode)) {
            continue
        }
        if ($seen.Add($normalizedSiteCode)) {
            $siteCodes.Add($normalizedSiteCode) | Out-Null
        }
    }

    return @($siteCodes.ToArray())
}

function Assert-PackageDepotInventorySchema {
<#
.SYNOPSIS
Validates the Package depot-inventory schema.
 
.DESCRIPTION
Checks that the internal depot inventory document uses the current shape with
inventoryVersion and acquisitionEnvironment.
 
.PARAMETER DepotInventoryDocumentInfo
The loaded depot-inventory document info.
 
.EXAMPLE
Assert-PackageDepotInventorySchema -DepotInventoryDocumentInfo $inventoryInfo
#>

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

    $document = $DepotInventoryDocumentInfo.Document
    if (-not $document.PSObject.Properties['inventoryVersion']) {
        throw "Package depot inventory '$($DepotInventoryDocumentInfo.Path)' is missing inventoryVersion."
    }
    if (-not $document.PSObject.Properties['acquisitionEnvironment'] -or $null -eq $document.acquisitionEnvironment) {
        throw "Package depot inventory '$($DepotInventoryDocumentInfo.Path)' is missing acquisitionEnvironment."
    }
    if ($document.acquisitionEnvironment.PSObject.Properties['environmentSources']) {
        foreach ($sourceProperty in @($document.acquisitionEnvironment.environmentSources.PSObject.Properties)) {
            if ($sourceProperty.Value -and $sourceProperty.Value.PSObject.Properties['priority']) {
                throw "Package depot inventory '$($DepotInventoryDocumentInfo.Path)' source '$($sourceProperty.Name)' still uses retired property 'priority'. Use 'searchOrder'."
            }
            if ($sourceProperty.Value -and -not $sourceProperty.Value.PSObject.Properties['searchOrder']) {
                throw "Package depot inventory '$($DepotInventoryDocumentInfo.Path)' source '$($sourceProperty.Name)' is missing searchOrder."
            }
            Assert-PackageEnvironmentSourceCapabilities -SourceId $sourceProperty.Name -SourceValue $sourceProperty.Value -DocumentPath $DepotInventoryDocumentInfo.Path
        }
    }
}

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

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

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

    if (-not [string]::Equals([string]$SourceValue.kind, 'filesystem', [System.StringComparison]::OrdinalIgnoreCase)) {
        return
    }

    foreach ($capabilityName in @('readable', 'writable', 'mirrorTarget', 'ensureExists')) {
        if (-not $SourceValue.PSObject.Properties[$capabilityName]) {
            throw "Package environment source '$SourceId' in '$DocumentPath' is missing capability '$capabilityName'."
        }
    }

    $writable = [bool]$SourceValue.writable
    if ([bool]$SourceValue.mirrorTarget -and -not $writable) {
        throw "Package environment source '$SourceId' in '$DocumentPath' cannot use mirrorTarget=true with writable=false."
    }
    if ([bool]$SourceValue.ensureExists -and -not $writable) {
        throw "Package environment source '$SourceId' in '$DocumentPath' cannot use ensureExists=true with writable=false."
    }
}

function Get-PackageDepotInventoryInfo {
<#
.SYNOPSIS
Loads the internal Package depot inventory.
 
.DESCRIPTION
Loads the effective local depot-inventory document, creating it from the
shipped module defaults when missing.
 
.EXAMPLE
Get-PackageDepotInventoryInfo
#>

    [CmdletBinding()]
    param()

    $inventoryPath = Get-PackageDepotInventoryPath
    $documentInfo = Read-PackageJsonDocument -Path $inventoryPath
    Assert-PackageDepotInventorySchema -DepotInventoryDocumentInfo $documentInfo
    return [pscustomobject]@{
        Path     = $documentInfo.Path
        Document = $documentInfo.Document
        Exists   = $true
    }
}

function Assert-PackageSourceInventorySchema {
<#
.SYNOPSIS
Validates the Package source-inventory schema.
 
.DESCRIPTION
Checks that the external inventory document uses the current shape with
inventoryVersion, global, and optional site overlays.
 
.PARAMETER SourceInventoryDocumentInfo
The loaded source-inventory document info.
 
.EXAMPLE
Assert-PackageSourceInventorySchema -SourceInventoryDocumentInfo $inventoryInfo
#>

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

    $document = $SourceInventoryDocumentInfo.Document
    if (-not $document.PSObject.Properties['inventoryVersion']) {
        throw "Package source inventory '$($SourceInventoryDocumentInfo.Path)' is missing inventoryVersion."
    }
    if (-not $document.PSObject.Properties['global']) {
        $document | Add-Member -MemberType NoteProperty -Name global -Value ([pscustomobject]@{})
    }
    if (-not $document.PSObject.Properties['sites']) {
        $document | Add-Member -MemberType NoteProperty -Name sites -Value ([pscustomobject]@{})
    }
}

function Get-PackageSourceInventoryInfo {
<#
.SYNOPSIS
Loads the optional external Package source inventory.
 
.DESCRIPTION
Resolves the effective inventory path, loads it when present, validates the
schema, and otherwise returns a null document marker.
 
.EXAMPLE
Get-PackageSourceInventoryInfo
#>

    [CmdletBinding()]
    param(
        [AllowNull()]
        [string]$ApplicationRootDirectory
    )

    $inventoryPath = Get-PackageSourceInventoryPath -ApplicationRootDirectory $ApplicationRootDirectory
    if (-not (Test-Path -LiteralPath $inventoryPath -PathType Leaf)) {
        return [pscustomobject]@{
            Path     = $inventoryPath
            Document = $null
            Exists   = $false
        }
    }

    $documentInfo = Read-PackageJsonDocument -Path $inventoryPath
    Assert-PackageSourceInventorySchema -SourceInventoryDocumentInfo $documentInfo
    return [pscustomobject]@{
        Path     = $documentInfo.Path
        Document = $documentInfo.Document
        Exists   = $true
    }
}

function Assert-PackageGlobalConfigSchema {
<#
.SYNOPSIS
Validates the Package global config schema.
 
.DESCRIPTION
Rejects retired global field names and requires the current Package
preferred-install and acquisition-environment fields.
 
.PARAMETER GlobalDocumentInfo
The loaded Package global config document info.
 
.EXAMPLE
Assert-PackageGlobalConfigSchema -GlobalDocumentInfo $globalInfo
#>

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

    if (-not $GlobalDocumentInfo.Document.PSObject.Properties['package'] -or $null -eq $GlobalDocumentInfo.Document.package) {
        throw "Package global config '$($GlobalDocumentInfo.Path)' does not contain a 'package' object."
    }

    $package = $GlobalDocumentInfo.Document.package
    foreach ($retiredProperty in @('managedStorageRoots', 'acquisitionDefaults', 'sourceBindings', 'downloadRootDirectory', 'installRootDirectory', 'allowSourceFallback', 'packageSelection', 'ownershipTracking')) {
        if ($package.PSObject.Properties[$retiredProperty]) {
            throw "Package global config '$($GlobalDocumentInfo.Path)' still uses retired property '$retiredProperty'."
        }
    }

    foreach ($requiredProperty in @('preferredTargetInstallDirectory', 'repositorySources', 'localRepositoryRoot', 'acquisitionEnvironment', 'packageState', 'selectionDefaults')) {
        if (-not $package.PSObject.Properties[$requiredProperty]) {
            throw "Package global config '$($GlobalDocumentInfo.Path)' is missing required property '$requiredProperty'."
        }
    }

    if ($package.selectionDefaults.PSObject.Properties['channel']) {
        throw "Package global config '$($GlobalDocumentInfo.Path)' still uses retired property 'selectionDefaults.channel'."
    }

    if ([string]::IsNullOrWhiteSpace([string]$package.preferredTargetInstallDirectory)) {
        throw "Package global config '$($GlobalDocumentInfo.Path)' is missing preferredTargetInstallDirectory."
    }
    if ($package.PSObject.Properties['applicationRootDirectory'] -and
        [string]::IsNullOrWhiteSpace([string]$package.applicationRootDirectory)) {
        throw "Package global config '$($GlobalDocumentInfo.Path)' defines an empty applicationRootDirectory."
    }
    if ($package.PSObject.Properties['shimDirectory'] -and
        [string]::IsNullOrWhiteSpace([string]$package.shimDirectory)) {
        throw "Package global config '$($GlobalDocumentInfo.Path)' defines an empty shimDirectory."
    }
    if ($package.PSObject.Properties['layout'] -and $package.layout) {
        foreach ($layoutProperty in @('packageDepotRelativePath', 'packageWorkSlotDirectory')) {
            if ($package.layout.PSObject.Properties[$layoutProperty] -and
                [string]::IsNullOrWhiteSpace([string]$package.layout.$layoutProperty)) {
                throw "Package global config '$($GlobalDocumentInfo.Path)' defines an empty layout.$layoutProperty."
            }
        }
    }

    foreach ($requiredAcquisitionProperty in @('stores', 'defaults')) {
        if (-not $package.acquisitionEnvironment.PSObject.Properties[$requiredAcquisitionProperty]) {
            throw "Package global config '$($GlobalDocumentInfo.Path)' is missing acquisitionEnvironment.$requiredAcquisitionProperty."
        }
    }

    if ($package.acquisitionEnvironment.stores.PSObject.Properties['defaultPackageDepotDirectory']) {
        throw "Package global config '$($GlobalDocumentInfo.Path)' still uses retired property 'acquisitionEnvironment.stores.defaultPackageDepotDirectory'. Use Configuration/Internal/DepotInventory.json environmentSources.defaultPackageDepot.basePath."
    }
    if ($package.acquisitionEnvironment.stores.PSObject.Properties['installWorkspaceDirectory']) {
        throw "Package global config '$($GlobalDocumentInfo.Path)' still uses retired property 'acquisitionEnvironment.stores.installWorkspaceDirectory'. Use 'acquisitionEnvironment.stores.packageFileStagingDirectory'."
    }
    if ($package.acquisitionEnvironment.stores.PSObject.Properties['installPreparationDirectory']) {
        throw "Package global config '$($GlobalDocumentInfo.Path)' still uses retired property 'acquisitionEnvironment.stores.installPreparationDirectory'. Use 'acquisitionEnvironment.stores.packageFileStagingDirectory' and 'acquisitionEnvironment.stores.packageInstallStageDirectory'."
    }
    if ($package.acquisitionEnvironment.defaults.PSObject.Properties['mirrorDownloadedArtifactsToDefaultPackageDepot']) {
        throw "Package global config '$($GlobalDocumentInfo.Path)' still uses retired property 'acquisitionEnvironment.defaults.mirrorDownloadedArtifactsToDefaultPackageDepot'. Use Configuration/Internal/DepotInventory.json environmentSources.<depotId>.mirrorTarget with writable=true."
    }

    foreach ($requiredStoreProperty in @('packageFileStagingDirectory', 'packageInstallStageDirectory')) {
        if (-not $package.acquisitionEnvironment.stores.PSObject.Properties[$requiredStoreProperty]) {
            throw "Package global config '$($GlobalDocumentInfo.Path)' is missing acquisitionEnvironment.stores.$requiredStoreProperty."
        }
    }

    foreach ($requiredDefaultProperty in @('allowFallback')) {
        if (-not $package.acquisitionEnvironment.defaults.PSObject.Properties[$requiredDefaultProperty]) {
            throw "Package global config '$($GlobalDocumentInfo.Path)' is missing acquisitionEnvironment.defaults.$requiredDefaultProperty."
        }
    }

    if ($package.acquisitionEnvironment.PSObject.Properties['tracking'] -and $package.acquisitionEnvironment.tracking) {
        if ($package.acquisitionEnvironment.tracking.PSObject.Properties['artifactIndexFilePath']) {
            throw "Package global config '$($GlobalDocumentInfo.Path)' still uses retired property 'acquisitionEnvironment.tracking.artifactIndexFilePath'. Package-file inventory is no longer durable state."
        }
        if ($package.acquisitionEnvironment.tracking.PSObject.Properties['packageFileIndexFilePath']) {
            throw "Package global config '$($GlobalDocumentInfo.Path)' still uses retired property 'acquisitionEnvironment.tracking.packageFileIndexFilePath'. Package-file inventory is resolved live from package definitions, config, and depot inventory."
        }
    }
    if ($package.packageState.PSObject.Properties['indexFilePath']) {
        throw "Package global config '$($GlobalDocumentInfo.Path)' still uses retired property 'packageState.indexFilePath'. Use 'packageState.inventoryFilePath'."
    }
    if (-not $package.packageState.PSObject.Properties['inventoryFilePath']) {
        throw "Package global config '$($GlobalDocumentInfo.Path)' is missing packageState.inventoryFilePath."
    }
    if (-not $package.packageState.PSObject.Properties['operationHistoryFilePath']) {
        throw "Package global config '$($GlobalDocumentInfo.Path)' is missing packageState.operationHistoryFilePath."
    }
    if (-not $package.selectionDefaults.PSObject.Properties['releaseTrack']) {
        throw "Package global config '$($GlobalDocumentInfo.Path)' is missing selectionDefaults.releaseTrack."
    }

    foreach ($repositoryProperty in @($package.repositorySources.PSObject.Properties)) {
        $repositorySource = $repositoryProperty.Value
        if (-not $repositorySource.PSObject.Properties['kind'] -or [string]::IsNullOrWhiteSpace([string]$repositorySource.kind)) {
            throw "Package global config '$($GlobalDocumentInfo.Path)' repositorySources.$($repositoryProperty.Name) is missing kind."
        }
        if (-not $repositorySource.PSObject.Properties['definitionRoot'] -or [string]::IsNullOrWhiteSpace([string]$repositorySource.definitionRoot)) {
            throw "Package global config '$($GlobalDocumentInfo.Path)' repositorySources.$($repositoryProperty.Name) is missing definitionRoot."
        }
    }

    if ($package.acquisitionEnvironment.PSObject.Properties['environmentSources'] -and $null -ne $package.acquisitionEnvironment.environmentSources) {
        foreach ($retiredEnvironmentSourceId in @('localPackageDepot', 'remotePackageDepot', 'corpPackageDepot', 'sitePackageDepot', 'vsCodeUpdateService')) {
            if ($package.acquisitionEnvironment.environmentSources.PSObject.Properties[$retiredEnvironmentSourceId]) {
                throw "Package global config '$($GlobalDocumentInfo.Path)' must not define acquisitionEnvironment.environmentSources.$retiredEnvironmentSourceId in the shipped global config."
            }
        }
    }
}