Private/Assets/Resolve-AvmPolicyBundle.ps1

function Resolve-AvmPolicyBundle {
    <#
    .SYNOPSIS
        Materialise a named policy bundle from the pinned policy library.

    .DESCRIPTION
        Projects the 'policyLibrary' section of the pin manifest
        (Resources/avm.pins.jsonc) into the PascalCase descriptor shape that
        Resolve-AvmPinnedAsset consumes, then materialises it.

        No repository configuration is required: the pinned library ships with
        the module, so 'avm check policy' works on a clean checkout.

        Template expansion for urlTemplate and archiveRoot:

          {repository} - e.g. 'Azure/policy-library-avm'
          {ref} - e.g. 'v1.0.0'
          {version} - the ref with any leading 'v' stripped, e.g. '1.0.0'

        The GitHub tarball wraps everything in '<repo>-<version>/', and
        Expand-AvmToolArchive does not strip that directory, so the effective
        sub-path handed to Resolve-AvmPinnedAsset is
        '<archiveRoot>/<bundles[Name]>'.

        AVM_POLICY_LIBRARY_REF overrides the pinned ref for testing an
        unreleased policy set. AVM_POLICY_LIBRARY_SHA256 is then mandatory --
        a ref without a matching hash is rejected so there is never an
        unverified download path.

    .PARAMETER Name
        Bundle name, e.g. 'avm-policy-aprl' or 'avm-policy-avmsec'. Must be a
        key of policyLibrary.bundles.

    .PARAMETER Pins
        Pre-loaded pin manifest. Defaults to Read-AvmPins.

    .PARAMETER AllowFileUrls
        Forwarded to Resolve-AvmPinnedAsset for fixtures.

    .OUTPUTS
        The pscustomobject returned by Resolve-AvmPinnedAsset.
    #>

    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param(
        [Parameter(Mandatory)]
        [string] $Name,

        [hashtable] $Pins,

        [switch] $AllowFileUrls
    )

    Set-StrictMode -Version 3.0
    $ErrorActionPreference = 'Stop'

    if (-not $Pins) { $Pins = Read-AvmPins }

    if (-not $Pins.ContainsKey('policyLibrary')) {
        throw [AvmConfigurationException]::new(
            "avm.pins: the pin manifest has no 'policyLibrary' section, so policy bundle '$Name' cannot be resolved.")
    }

    $library = $Pins['policyLibrary']
    if (-not $library.ContainsKey('bundles') -or -not $library['bundles'].ContainsKey($Name)) {
        $known = if ($library.ContainsKey('bundles')) { ($library['bundles'].Keys | Sort-Object) -join "', '" } else { '' }
        throw [AvmConfigurationException]::new(
            "avm.pins: policy bundle '$Name' is not declared in policyLibrary.bundles (known: '$known').")
    }

    $ref = [string]$library['ref']
    $sha = [string]$library['sha256']

    # Paired override: a ref without its hash would mean an unverified download.
    $refOverride = $env:AVM_POLICY_LIBRARY_REF
    $shaOverride = $env:AVM_POLICY_LIBRARY_SHA256
    if (-not [string]::IsNullOrWhiteSpace($refOverride)) {
        if ([string]::IsNullOrWhiteSpace($shaOverride)) {
            throw [AvmConfigurationException]::new(
                'AVM_POLICY_LIBRARY_REF is set but AVM_POLICY_LIBRARY_SHA256 is not. Both are required so the override archive is still checksum-verified.')
        }
        $ref = $refOverride.Trim()
        $sha = $shaOverride.Trim().ToLowerInvariant()
    }
    elseif (-not [string]::IsNullOrWhiteSpace($shaOverride)) {
        throw [AvmConfigurationException]::new(
            'AVM_POLICY_LIBRARY_SHA256 is set but AVM_POLICY_LIBRARY_REF is not. Both are required so the override is unambiguous.')
    }

    $version = $ref -replace '^v', ''
    $expand = {
        param([string] $Template)
        $Template.
        Replace('{repository}', [string]$library['repository']).
        Replace('{ref}', $ref).
        Replace('{version}', $version)
    }

    $source = & $expand ([string]$library['urlTemplate'])
    $archiveRoot = & $expand ([string]$library['archiveRoot'])

    $bundlePath = [string]$library['bundles'][$Name]
    $subPath = if ([string]::IsNullOrWhiteSpace($archiveRoot)) { $bundlePath } else { "$archiveRoot/$bundlePath" }

    $descriptor = [pscustomobject]@{
        Source = $source
        Ref    = $ref
        Sha256 = $sha
        Path   = $subPath
        Type   = 'archive'
    }

    Resolve-AvmPinnedAsset -Name $Name -Asset $descriptor -AllowFileUrls:$AllowFileUrls
}