Private/Test-DFToolIdentityGuideSchema.ps1

#Requires -Version 7.0

function Test-DFToolIdentityGuideSchema {
    <#
    .SYNOPSIS
        Validates a merged tool-identity-guide document. Returns $true if
        valid; populates -Errors with any violations.
    .DESCRIPTION
        Checks structural shape (schemaVersion, per-tool packages/linkedVia)
        plus one cross-tool invariant: no (source, packageId) pair may be
        claimed by more than one tool key — that would mean the guide itself
        contradicts the safety property it exists to provide.
    .PARAMETER Database
        The parsed tool-identity-guide document (schemaVersion, tools).
    .PARAMETER Errors
        Reference to an array populated with violation messages. Empty when
        validation passes.
    .OUTPUTS
        [bool] - $true if valid, $false if any violations found.
    #>

    [CmdletBinding()]
    [OutputType([bool])]
    param(
        [Parameter(Mandatory)][PSCustomObject]$Database,
        [ref]$Errors
    )

    $errs = [System.Collections.Generic.List[string]]::new()

    function PSProp ([PSCustomObject]$obj, [string]$key) {
        if ($null -eq $obj) { return $null }
        $p = $obj.PSObject.Properties[$key]
        if ($p) { return $p.Value } else { return $null }
    }

    if (-not (PSProp $Database 'schemaVersion')) { $errs.Add('Missing required field: schemaVersion') }
    elseif ((PSProp $Database 'schemaVersion') -ne 1) { $errs.Add("Unsupported schemaVersion '$($Database.schemaVersion)' — expected 1") }

    $tools = PSProp $Database 'tools'
    $validLinkedVia = @('repo', 'homepage', 'curated')
    $seenPairs = @{}

    if (-not $tools) {
        $errs.Add('Missing required field: tools')
    } else {
        foreach ($prop in $tools.PSObject.Properties) {
            $key = $prop.Name
            $entry = $prop.Value

            $packages = PSProp $entry 'packages'
            $packageProps = $packages ? @($packages.PSObject.Properties) : @()
            if ($packageProps.Count -eq 0) {
                $errs.Add("$key`: packages must be a non-empty object")
            } else {
                foreach ($pkgProp in $packageProps) {
                    $pairKey = "$($pkgProp.Name.ToLowerInvariant()):$(([string]$pkgProp.Value).ToLowerInvariant())"
                    if ($seenPairs.ContainsKey($pairKey)) {
                        $errs.Add("$pairKey claimed by both '$($seenPairs[$pairKey])' and '$key'")
                    } else {
                        $seenPairs[$pairKey] = $key
                    }
                }
            }

            $linkedVia = PSProp $entry 'linkedVia'
            if (-not $linkedVia) { $errs.Add("$key`: missing required field linkedVia") }
            elseif ($linkedVia -notin $validLinkedVia) { $errs.Add("$key`: invalid linkedVia '$linkedVia'. Valid: $($validLinkedVia -join ', ')") }

            $repoProp = $entry.PSObject.Properties['repo']
            if ($repoProp -and -not $repoProp.Value) { $errs.Add("$key`: repo, when present, must be a non-empty string") }
        }
    }

    if ($Errors) { $Errors.Value = $errs.ToArray() }
    return $errs.Count -eq 0
}