Engines/Bicep/BicepWorkflow.ps1

function Resolve-AvmBicepWorkflowPath {
    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory)] [string] $Root,
        [Parameter(Mandatory)] [string] $RelativePath,
        [switch] $AllowMissing
    )

    $relative = $RelativePath.Replace('\', '/')
    if ([IO.Path]::IsPathRooted($relative) -or $relative -match '[:\x00-\x1f*?\[\]]' -or
        $relative.Split('/') -contains '..' -or [string]::IsNullOrWhiteSpace($relative)) {
        throw [ArgumentException]::new('A workflow path must be a literal, repository-relative path without parent segments.')
    }
    $rootPath = [IO.Path]::GetFullPath($Root)
    $path = [IO.Path]::GetFullPath((Join-Path $rootPath $relative))
    $prefix = $rootPath.TrimEnd([IO.Path]::DirectorySeparatorChar) + [IO.Path]::DirectorySeparatorChar
    if (-not $path.StartsWith($prefix, [StringComparison]::Ordinal)) {
        throw [ArgumentException]::new('The workflow path must remain inside its checkout.')
    }
    $current = $rootPath
    foreach ($segment in $relative.Split('/', [StringSplitOptions]::RemoveEmptyEntries)) {
        $current = Join-Path $current $segment
        if ((Test-Path -LiteralPath $current) -and
            ((Get-Item -LiteralPath $current -Force).Attributes -band [IO.FileAttributes]::ReparsePoint)) {
            throw [ArgumentException]::new('Workflow inputs must not traverse linked files or directories.')
        }
    }
    if (-not $AllowMissing -and -not (Test-Path -LiteralPath $path)) {
        throw [IO.FileNotFoundException]::new("Workflow input does not exist: $relative")
    }
    return $path
}

function ConvertTo-AvmBicepWorkflowBooleanInternal {
    [CmdletBinding()]
    [OutputType([bool])]
    param([Parameter(Mandatory)] [object] $Value)

    if ($Value -is [bool]) { return $Value }
    if ($Value -is [string] -and $Value -match '^(true|false)$') { return $Value -ieq 'true' }
    throw [ArgumentException]::new('A workflow boolean must be true or false.')
}

function Test-AvmBicepWorkflowLegacyModuleLockInternal {
    [CmdletBinding()]
    [OutputType([bool])]
    param([Parameter(Mandatory)] [Collections.IDictionary] $Template)

    $resources = @(if ($Template['resources'] -is [Collections.IDictionary]) {
            $Template.resources.Values
        }
        else { $Template['resources'] | Where-Object { $null -ne $_ } })
    if ($resources.Count -gt 0 -and $Template['$schema'] -match '/(?:managementGroup|tenant)DeploymentTemplate\.json#?$') {
        return $true
    }
    foreach ($resource in $resources) {
        if ($resource -isnot [Collections.IDictionary]) {
            throw [ArgumentException]::new('Compiled deployment resources must be objects.')
        }
        if ($resource['type'] -ieq 'Microsoft.Resources/deployments') {
            $properties = $resource['properties']
            if ($properties -isnot [Collections.IDictionary]) {
                throw [ArgumentException]::new('Compiled nested deployments must have properties.')
            }
            if ($properties['templateLink']) { return $true }
            $nested = $properties['template']
            if ($nested -is [string] -and $nested -cmatch "^\[variables\('([^']+)'\)\]$" -and
                $Template['variables'] -is [Collections.IDictionary]) {
                $nested = $Template['variables'][$Matches[1]]
            }
            if ($nested -isnot [Collections.IDictionary]) { return $true }
            if (Test-AvmBicepWorkflowLegacyModuleLockInternal -Template $nested) { return $true }
        }
        if ($resource['resources'] -and (Test-AvmBicepWorkflowLegacyModuleLockInternal -Template @{ resources = $resource.resources })) {
            return $true
        }
    }
    return $false
}

function Get-AvmBicepWorkflowNamePrefixInternal {
    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory)] [ValidateSet('isolated', 'shared', 'fallback')] [string] $IsolationMode,
        [AllowEmptyString()] [string] $Prefix
    )

    if ($IsolationMode -eq 'fallback') { return '' }
    if ([string]::IsNullOrEmpty($Prefix)) { $Prefix = 'gci' }
    if ($Prefix -cnotmatch '^[a-z][a-z0-9]{0,2}$') {
        throw [ArgumentException]::new('Reviewed Bicep tests require a lowercase alphanumeric naming prefix of one to three characters.')
    }
    return $Prefix + '#_avmE2eSuffix_#'
}

function Assert-AvmBicepWorkflowIsolationInputInternal {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [ValidateSet('isolated', 'shared', 'fallback')] [string] $IsolationMode,
        [Parameter(Mandatory)] [string] $CompiledTemplate,
        [bool] $RemoveDeployment = $true
    )

    if ($IsolationMode -eq 'fallback') { return }
    if ($IsolationMode -eq 'shared' -and -not $RemoveDeployment) {
        throw [ArgumentException]::new('Shared-target execution requires cleanup before releasing the lifecycle lock; removeDeployment must be true.')
    }
    $template = ConvertFrom-Json -InputObject $CompiledTemplate -AsHashtable
    $ci = Get-AvmBicepWorkflowEnvironment -Enabled
    if (@($ci.Tokens.Keys | Where-Object { $_ -ine 'namePrefix' }).Count -gt 0) {
        throw [ArgumentException]::new('Reviewed isolation does not allow custom localToken overrides.')
    }
    if (-not [string]::IsNullOrEmpty($ci.KeyVaultName)) {
        throw [ArgumentException]::new('Reviewed isolation does not allow implicit CI Key Vault parameter overrides.')
    }
    $parameters = @{}
    if ($template['parameters'] -is [Collections.IDictionary]) {
        foreach ($name in $template['parameters'].Keys) { $parameters[$name] = @{ type = 'secureString' } }
    }
    $overrides = Get-AvmBicepCiParameter -TemplateParameters $parameters -Variables $ci.Variables -Secrets $ci.Secrets
    if ($overrides.Count -gt 0) {
        throw [ArgumentException]::new('Reviewed isolation does not allow CI parameter overrides of compiled test defaults.')
    }
}

function Invoke-AvmBicepWorkflowGit {
    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param(
        [Parameter(Mandatory)] [string] $Root,
        [Parameter(Mandatory)] [string[]] $Arguments,
        [switch] $Remote,
        [switch] $AllowFailure
    )

    $maximum = if ($Remote) { 3 } else { 1 }
    $git = (Get-Command -Name git -CommandType Application -ErrorAction Stop | Select-Object -First 1).Source
    for ($attempt = 1; $attempt -le $maximum; $attempt++) {
        try {
            $result = Invoke-AvmProcess -FilePath $git -ArgumentList $Arguments -WorkingDirectory $Root `
                -EnvVars @{ GIT_TERMINAL_PROMPT = '0' } -TimeoutSec 120 -IgnoreExitCode
        }
        catch [TimeoutException] {
            if ($attempt -eq $maximum) { throw }
            Write-Warning "Git $($Arguments[0]) timed out on attempt $attempt of $maximum; retrying the read."
            Start-Sleep -Seconds (2 * $attempt)
            continue
        }
        if ($result.ExitCode -eq 0) { return $result }
        if ($attempt -lt $maximum) { Start-Sleep -Seconds (2 * $attempt) }
    }
    if ($AllowFailure) { return $result }
    throw [InvalidOperationException]::new("Git $($Arguments[0]) failed after $maximum attempt(s): $($result.StdErr)")
}

function Get-AvmBicepWorkflowTestFilesInternal {
    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAlignAssignmentStatement', '',
        Justification = 'Compact workflow matrix fields intentionally use their public JSON names.')]
    [OutputType([hashtable])]
    param(
        [Parameter(Mandatory)] [string] $Root,
        [Parameter(Mandatory)] [string] $ModulePath,
        [string] $PsRuleFilterRegex = '(defaults|waf-aligned)'
    )

    $module = Resolve-AvmBicepWorkflowPath -Root $Root -RelativePath $ModulePath
    $filter = [regex]::new($PsRuleFilterRegex, [Text.RegularExpressions.RegexOptions]::None,
        [TimeSpan]::FromSeconds(1))
    $files = @(Get-ChildItem -LiteralPath $module -Recurse -File -Filter 'main.test.bicep' |
            Sort-Object FullName | ForEach-Object {
                $relative = [IO.Path]::GetRelativePath($module, $_.FullName).Replace('\', '/')
                $null = Resolve-AvmBicepWorkflowPath -Root $module -RelativePath $relative
                [ordered]@{
                    path = $relative
                    name = $_.Directory.Name
                    e2eIgnore = Test-Path -LiteralPath (Join-Path $_.Directory.FullName '.e2eignore')
                }
            })
    $policyFiles = @($files | Where-Object { $filter.IsMatch($_.path) } |
            ForEach-Object { [ordered]@{ path = $_.path; name = $_.name } })
    return @{
        moduleTestFilePaths = ConvertTo-Json -InputObject $files -Compress
        psRuleModuleTestFilePaths = ConvertTo-Json -InputObject $policyFiles -Compress
    }
}

function Get-AvmBicepWorkflowModuleMatrixInternal {
    [CmdletBinding(DefaultParameterSetName = 'Changed')]
    [OutputType([hashtable])]
    param(
        [Parameter(Mandatory)] [string] $Root,
        [Parameter(Mandatory, ParameterSetName = 'Requested')] [string] $ModulePaths,
        [Parameter(Mandatory, ParameterSetName = 'Changed')] [AllowEmptyCollection()] [string[]] $ChangedFiles,
        [switch] $ExcludeMetadataChanges
    )

    $requested = $PSCmdlet.ParameterSetName -eq 'Requested'
    $paths = if ($requested) { $ModulePaths.Split(',') } else { $ChangedFiles }
    $modules = [Collections.Generic.SortedSet[string]]::new([StringComparer]::Ordinal)
    foreach ($path in $paths) {
        if ([string]::IsNullOrWhiteSpace($path)) {
            if ($requested) { throw [ArgumentException]::new('Every requested module path must be nonempty.') }
            continue
        }
        $relative = $path.Trim().Replace('\', '/').TrimEnd('/')
        if ($relative.StartsWith('./', [StringComparison]::Ordinal)) { $relative = $relative.Substring(2) }
        if ($relative.Split('/') -contains '..' -or $relative -match '[*?\[\]\x00-\x1f]') {
            throw [ArgumentException]::new('Module paths must not contain parent segments, wildcards or control characters.')
        }
        if (-not $requested -and ($relative -imatch '/README\.md$' -or
                ($ExcludeMetadataChanges -and $relative -imatch '/metadata\.json$'))) { continue }
        $match = [regex]::Match($relative, '^(avm/(?:res|ptn|utl)/[a-z0-9-]+/[a-z0-9-]+)(?:/|$)')
        if (-not $match.Success) {
            if ($requested) { throw [ArgumentException]::new("Not a top-level AVM module: $relative") }
            continue
        }
        $modulePath = $match.Groups[1].Value
        $source = Resolve-AvmBicepWorkflowPath -Root $Root -RelativePath "$modulePath/main.bicep" -AllowMissing
        if (-not (Test-Path -LiteralPath $source -PathType Leaf)) {
            if ($requested) { throw [ArgumentException]::new("No top-level AVM module exists at $modulePath.") }
            continue
        }
        $null = $modules.Add($modulePath)
    }
    if ($modules.Count -gt 256) {
        throw [ArgumentException]::new('The selection exceeds the 256-job matrix limit. Use smaller manual module batches.')
    }
    $entries = @(foreach ($modulePath in $modules) {
            $entry = Get-AvmBicepWorkflowTestFilesInternal -Root $Root -ModulePath $modulePath
            $entry.modulePath = $modulePath
            $entry
        })
    return @{ include = $entries }
}

function Get-AvmBicepWorkflowRemoteTags {
    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '',
        Justification = 'The function returns the complete remote tag collection.')]
    [OutputType([hashtable])]
    param(
        [Parameter(Mandatory)] [string] $Root,
        [Parameter(Mandatory)] [string] $Pattern
    )

    $result = Invoke-AvmBicepWorkflowGit -Root $Root -Arguments @('ls-remote', '--tags', 'origin', $Pattern) -Remote
    $tags = @{}
    $peeled = @{}
    foreach ($line in $result.StdOut -split '\r?\n') {
        if ($line -ceq '') { continue }
        $match = [regex]::Match($line, '^([0-9a-f]{40})\s+refs/tags/(.+?)(\^\{\})?$')
        if (-not $match.Success) { throw [InvalidOperationException]::new('Git returned an invalid remote tag record.') }
        $name = $match.Groups[2].Value
        if ($match.Groups[3].Success) { $peeled[$name] = $match.Groups[1].Value }
        else { $tags[$name] = $match.Groups[1].Value }
    }
    foreach ($name in $peeled.Keys) { $tags[$name] = $peeled[$name] }
    return $tags
}

function Get-AvmBicepWorkflowPublishableChangesInternal {
    [CmdletBinding()]
    [OutputType([object[]])]
    param(
        [Parameter(Mandatory)] [string] $Root,
        [Parameter(Mandatory)] [string] $ModulePath,
        [Parameter(Mandatory)] [string] $TargetCommit,
        [string] $BaseCommit,
        [switch] $InitialRelease,
        [switch] $ExactModule
    )

    if ($TargetCommit -cnotmatch '^[0-9a-f]{40}$' -or
        (-not $InitialRelease -and $BaseCommit -cnotmatch '^[0-9a-f]{40}$')) {
        throw [ArgumentException]::new('Publishable change discovery requires full commit identifiers or an initial release.')
    }
    $arguments = if ($InitialRelease) {
        @('ls-tree', '-r', '--name-only', $TargetCommit, '--', $ModulePath)
    }
    else {
        @('diff', '--no-renames', '--name-only', '--diff-filter=ACMRT', $BaseCommit, $TargetCommit, '--', $ModulePath)
    }
    $files = Invoke-AvmBicepWorkflowGit -Root $Root -Arguments $arguments
    $candidates = [Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal)
    foreach ($changed in $files.StdOut -split '\r?\n') {
        if ($changed -cnotmatch '/(?:main|version)\.json$') { continue }
        $relative = $changed.Substring(0, $changed.LastIndexOf('/'))
        if ($ExactModule -and $relative -cne $ModulePath) { continue }
        $path = Resolve-AvmBicepWorkflowPath -Root $Root -RelativePath $relative
        if ((Test-Path -LiteralPath (Join-Path $path 'main.json') -PathType Leaf) -and
            (Test-Path -LiteralPath (Join-Path $path 'version.json') -PathType Leaf)) {
            $null = $candidates.Add($relative)
        }
    }
    return @($candidates | Sort-Object @{ Expression = { $_.Length }; Descending = $true }, @{ Expression = { $_ } })
}

function Get-AvmBicepWorkflowPendingReleasePlanInternal {
    [CmdletBinding()]
    [OutputType([pscustomobject[]])]
    param(
        [Parameter(Mandatory)] [string] $Root,
        [Parameter(Mandatory)] [string] $ModulePath,
        [Parameter(Mandatory)] [string] $TargetCommit
    )

    if ($TargetCommit -cnotmatch '^[0-9a-f]{40}$') {
        throw [ArgumentException]::new('Pending release discovery requires a full target commit identifier.')
    }
    $null = Resolve-AvmBicepWorkflowPath -Root $Root -RelativePath $ModulePath
    $shallow = Invoke-AvmBicepWorkflowGit -Root $Root -Arguments @('rev-parse', '--is-shallow-repository')
    if ($shallow.StdOut.Trim() -cne 'false') {
        throw [InvalidOperationException]::new('Pending release discovery requires complete Git history.')
    }
    $files = Invoke-AvmBicepWorkflowGit -Root $Root -Arguments @('ls-tree', '-r', '--name-only', $TargetCommit, '--', $ModulePath)
    $paths = @($files.StdOut -split '\r?\n' | Where-Object { $_ -cmatch '/main\.json$' } |
            ForEach-Object { $_.Substring(0, $_.LastIndexOf('/')) } |
            Where-Object {
                Test-Path -LiteralPath (Join-Path -Path $Root -ChildPath $_ -AdditionalChildPath 'version.json') -PathType Leaf
            } |
            Sort-Object @{ Expression = { $_.Length }; Descending = $true }, @{ Expression = { $_ } })
    $tags = Get-AvmBicepWorkflowRemoteTags -Root $Root -Pattern "$ModulePath/*"
    $plan = [Collections.Generic.List[object]]::new()
    foreach ($relative in $paths) {
        $pattern = "^$([regex]::Escape($relative))/\d+\.\d+\.\d+$"
        $latest = @($tags.Keys | Where-Object { $_ -cmatch $pattern } |
                Sort-Object { [version]($_.Substring($_.LastIndexOf('/') + 1)) } -Descending |
                Select-Object -First 1)
        $base = if ($latest.Count -eq 0) { '' } else { $tags[$latest[0]] }
        if ($base) {
            $available = Invoke-AvmBicepWorkflowGit -Root $Root -Arguments @('cat-file', '-e', "${base}^{commit}") -AllowFailure
            if ($available.ExitCode -ne 0) {
                $null = Invoke-AvmBicepWorkflowGit -Root $Root -Arguments @('fetch', '--no-tags', 'origin', $base) -Remote
                $null = Invoke-AvmBicepWorkflowGit -Root $Root -Arguments @('cat-file', '-e', "${base}^{commit}")
            }
            $ancestor = Invoke-AvmBicepWorkflowGit -Root $Root -Arguments @('merge-base', '--is-ancestor', $base, $TargetCommit) -AllowFailure
            if ($ancestor.ExitCode -ne 0) {
                throw [InvalidOperationException]::new("The latest release tag for $relative is not an ancestor of the release target.")
            }
            foreach ($file in @('main.json', 'version.json')) {
                $null = Invoke-AvmBicepWorkflowGit -Root $Root -Arguments @('cat-file', '-e', "${base}:$relative/$file")
            }
        }
        $changes = @(Get-AvmBicepWorkflowPublishableChangesInternal -Root $Root -ModulePath $relative `
                -TargetCommit $TargetCommit -BaseCommit $base -InitialRelease:($latest.Count -eq 0) -ExactModule)
        if ($changes.Count -gt 0) {
            $plan.Add([pscustomobject]@{ ModulePath = $relative; BaseCommit = $base; InitialRelease = $latest.Count -eq 0 })
        }
    }
    return $plan.ToArray()
}

function Assert-AvmBicepWorkflowReleaseContext {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [string] $Root,
        [Parameter(Mandatory)] [string] $TargetCommit
    )

    if ($TargetCommit -cnotmatch '^[0-9a-f]{40}$') {
        throw [ArgumentException]::new('Release target commits must be full SHA-1 identifiers.')
    }
    if ($env:GITHUB_REPOSITORY -cne 'Azure/bicep-registry-modules' -or $env:GITHUB_REF -cne 'refs/heads/main') {
        throw [InvalidOperationException]::new('Module release tags are restricted to the upstream registry main branch.')
    }
    $head = (Invoke-AvmBicepWorkflowGit -Root $Root -Arguments @('rev-parse', 'HEAD')).StdOut.Trim()
    if ($head -cne $TargetCommit) {
        throw [InvalidOperationException]::new('Release inputs must be inspected from the exact target commit checkout.')
    }
    $remote = (Invoke-AvmBicepWorkflowGit -Root $Root -Arguments @('remote', 'get-url', '--push', 'origin')).StdOut.Trim()
    if ($remote -cnotmatch '^https://github\.com/Azure/bicep-registry-modules(?:\.git)?$') {
        throw [InvalidOperationException]::new('The release remote must be the upstream registry HTTPS URL.')
    }
}

function Assert-AvmBicepWorkflowReleaseChangelog {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [string] $Root,
        [Parameter(Mandatory)] [string] $ModulePath,
        [Parameter(Mandatory)] [string] $Version
    )

    $context = "CHANGELOG.md for $ModulePath release $Version"
    $path = Resolve-AvmBicepWorkflowPath -Root $Root -RelativePath $ModulePath
    try {
        $data = Get-AvmBicepVersionInput -Scope @{ Path = $path; ModuleRelativePath = $ModulePath }
    }
    catch [IO.IOException], [UnauthorizedAccessException], [Text.DecoderFallbackException],
    [Management.Automation.CommandNotFoundException], [Management.Automation.ItemNotFoundException] {
        throw [InvalidOperationException]::new("${context}: cannot read changelog data: $($_.Exception.Message)", $_.Exception)
    }
    if ($null -eq $data -or $data.VersionError) {
        throw [ArgumentException]::new("${context}: cannot inspect valid version.json data.")
    }
    if ($data.ChangelogFiles.Count -ne 1 -or $data.ChangelogFiles[0].Name -cne 'CHANGELOG.md' -or
        $data.ChangelogFiles[0].PSIsContainer -or
        ($data.ChangelogFiles[0].Attributes -band [IO.FileAttributes]::ReparsePoint)) {
        throw [ArgumentException]::new("${context}: a regular exact-case changelog file is required.")
    }
    if ($data.ChangelogError) {
        throw [ArgumentException]::new("${context}: changelog is unreadable as UTF-8: $($data.ChangelogError)")
    }
    if (@($data.Headings | Where-Object { $null -eq $_.Version }).Count -gt 0) {
        throw [ArgumentException]::new("${context}: release headings must use '## MAJOR.MINOR.PATCH'.")
    }
    $heading = "^## $([regex]::Escape($Version))\s*$"
    $releases = @($data.Versions | Where-Object { $_.Text -cmatch $heading })
    if ($releases.Count -ne 1) {
        throw [ArgumentException]::new("${context}: exactly one expected-version section is required.")
    }
    $sections = $releases[0].Sections
    foreach ($section in $sections) {
        if ($section.Positions.Count -ne 1 -or
            @($section.Content | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }).Count -eq 0) {
            throw [ArgumentException]::new("${context}: exactly one nonempty '$($section.Name)' section is required.")
        }
    }
    if ($sections[0].Positions[0] -ge $sections[1].Positions[0]) {
        throw [ArgumentException]::new("${context}: Changes must precede Breaking Changes.")
    }
}

function Initialize-AvmBicepWorkflowMatrixInternal {
    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAlignAssignmentStatement', '',
        Justification = 'The output fields preserve the established workflow-output contract.')]
    [OutputType([hashtable])]
    param(
        [Parameter(Mandatory)] [string] $Root,
        [Parameter(Mandatory)] [ValidateSet('push', 'pull_request', 'workflow_dispatch')] [string] $EventName,
        [Parameter(Mandatory)] [string] $TargetCommit,
        [string] $BaseCommit,
        [string] $PullRequestHeadCommit,
        [string] $ModulePaths,
        [bool] $CreateReleaseTag = $false,
        [bool] $PublishOnly = $false,
        [bool] $StaticValidation = $true,
        [bool] $DeploymentValidation = $true,
        [bool] $RemoveDeployment = $true,
        [string] $CustomLocation
    )

    if ($TargetCommit -cnotmatch '^[0-9a-f]{40}$' -or
        ($EventName -eq 'push' -and -not [string]::IsNullOrEmpty($BaseCommit) -and $BaseCommit -cnotmatch '^[0-9a-f]{40}$')) {
        throw [ArgumentException]::new('Workflow commits must be full SHA-1 commit identifiers.')
    }
    if ($EventName -eq 'pull_request' -and $PullRequestHeadCommit -cnotmatch '^[0-9a-f]{40}$') {
        throw [ArgumentException]::new('The pull request head must be a full SHA-1 commit identifier.')
    }
    if ($PublishOnly -and -not $CreateReleaseTag) {
        throw [ArgumentException]::new('publishOnly requires createReleaseTag.')
    }
    if ($CreateReleaseTag -and -not $PublishOnly -and (-not $StaticValidation -or -not $DeploymentValidation)) {
        throw [ArgumentException]::new('createReleaseTag requires staticValidation and deploymentValidation unless publishOnly is selected.')
    }
    $head = (Invoke-AvmBicepWorkflowGit -Root $Root -Arguments @('rev-parse', 'HEAD')).StdOut.Trim()
    if ($head -cne $TargetCommit) {
        throw [InvalidOperationException]::new('Module discovery requires the checkout to match targetCommit exactly.')
    }
    $includeAll = $false
    if ($EventName -eq 'workflow_dispatch') {
        $matrix = Get-AvmBicepWorkflowModuleMatrixInternal -Root $Root -ModulePaths $ModulePaths
        $BaseCommit = ''
    }
    else {
        if ($EventName -eq 'pull_request') {
            $history = Invoke-AvmBicepWorkflowGit -Root $Root -Arguments @('rev-list', '--parents', '-n', '1', $TargetCommit, '--') -AllowFailure
            $records = @($history.StdOut -split '\r?\n' | Where-Object { $_ -cne '' })
            if ($history.ExitCode -ne 0 -or $records.Count -ne 1) {
                throw [InvalidOperationException]::new('The tested pull request merge commit is unavailable.')
            }
            $parents = $records[0].Split(' ', [StringSplitOptions]::RemoveEmptyEntries)
            if ($parents.Count -ne 3) {
                throw [InvalidOperationException]::new('The tested pull request merge commit must have exactly two parents.')
            }
            if ($parents[0] -cne $TargetCommit -or $parents[1] -cnotmatch '^[0-9a-f]{40}$' -or
                $parents[2] -cnotmatch '^[0-9a-f]{40}$') {
                throw [InvalidOperationException]::new('The tested pull request merge history is malformed.')
            }
            if ($parents[2] -cne $PullRequestHeadCommit) {
                throw [InvalidOperationException]::new('The tested merge second parent does not match the pull request head.')
            }
            $BaseCommit = $parents[1]
        }
        $available = -not [string]::IsNullOrEmpty($BaseCommit) -and $BaseCommit -notmatch '^0+$'
        if ($available) {
            $available = (Invoke-AvmBicepWorkflowGit -Root $Root -Arguments @('cat-file', '-e', "${BaseCommit}^{commit}") -AllowFailure).ExitCode -eq 0
            if (-not $available) {
                $fetch = Invoke-AvmBicepWorkflowGit -Root $Root -Arguments @('fetch', '--no-tags', 'origin', $BaseCommit) -Remote -AllowFailure
                if ($fetch.ExitCode -eq 0) {
                    $available = (Invoke-AvmBicepWorkflowGit -Root $Root -Arguments @('cat-file', '-e', "${BaseCommit}^{commit}") -AllowFailure).ExitCode -eq 0
                }
            }
        }
        if (-not $available -and $EventName -eq 'pull_request') {
            throw [InvalidOperationException]::new('The pull request base commit is unavailable.')
        }
        if ($available) {
            $git = Invoke-AvmBicepWorkflowGit -Root $Root -Arguments @(
                'diff', '--no-renames', '--name-only', '--diff-filter=ACMRTD', $BaseCommit, $TargetCommit, '--', 'avm')
        }
        else {
            Write-Warning 'The push base is unavailable. Selecting all current modules for static validation and release preview only.'
            $git = Invoke-AvmBicepWorkflowGit -Root $Root -Arguments @('ls-tree', '-r', '--name-only', $TargetCommit, '--', 'avm')
            $BaseCommit = ''
            $includeAll = $true
            $DeploymentValidation = $false
        }
        $changedFiles = @($git.StdOut -split '\r?\n' | Where-Object { $_ -cne '' })
        $matrix = Get-AvmBicepWorkflowModuleMatrixInternal -Root $Root -ChangedFiles $changedFiles `
            -ExcludeMetadataChanges:($EventName -ne 'pull_request')
    }
    return @{
        baseCommit = $BaseCommit
        targetCommit = $TargetCommit
        hasModules = ($matrix.include.Count -gt 0).ToString().ToLowerInvariant()
        includeAllVersionedModules = $includeAll.ToString().ToLowerInvariant()
        moduleMatrix = ConvertTo-Json -InputObject $matrix -Depth 10 -Compress
        workflowInput = ConvertTo-Json -InputObject @{
            customLocation = $CustomLocation
            staticValidation = $StaticValidation.ToString().ToLowerInvariant()
            deploymentValidation = $DeploymentValidation.ToString().ToLowerInvariant()
            removeDeployment = $RemoveDeployment.ToString().ToLowerInvariant()
        } -Compress
    }
}

function Initialize-AvmBicepReleaseMatrixInternal {
    [CmdletBinding()]
    [OutputType([hashtable])]
    param(
        [Parameter(Mandatory)] [string] $Root,
        [Parameter(Mandatory)] [ValidateSet('workflow_dispatch', 'push')] [string] $EventName,
        [Parameter(Mandatory)] [string] $TargetCommit,
        [string] $BaseCommit
    )

    Assert-AvmBicepWorkflowReleaseContext -Root $Root -TargetCommit $TargetCommit
    $dirty = Invoke-AvmBicepWorkflowGit -Root $Root -Arguments @('status', '--porcelain', '--untracked-files=all', '--', 'avm')
    if (-not [string]::IsNullOrWhiteSpace($dirty.StdOut)) {
        throw [InvalidOperationException]::new('Release discovery requires module files unchanged from the target commit.')
    }
    if ($EventName -ceq 'push') {
        $paths = @(Get-AvmBicepWorkflowPublishableChangesInternal -Root $Root -ModulePath avm `
                -TargetCommit $TargetCommit -BaseCommit $BaseCommit)
    }
    else {
        $paths = @(Get-AvmBicepWorkflowPendingReleasePlanInternal -Root $Root -ModulePath avm -TargetCommit $TargetCommit |
                ForEach-Object { $_.ModulePath })
    }
    $modules = [Collections.Generic.SortedSet[string]]::new([StringComparer]::Ordinal)
    foreach ($path in $paths) {
        $match = [regex]::Match($path, '^(avm/(?:res|ptn|utl)/[a-z0-9-]+/[a-z0-9-]+)(?:/|$)')
        if ($match.Success) {
            $null = Resolve-AvmBicepWorkflowPath -Root $Root -RelativePath "$($match.Groups[1].Value)/main.bicep"
            $null = $modules.Add($match.Groups[1].Value)
        }
    }
    if ($modules.Count -gt 256) {
        throw [InvalidOperationException]::new('The release selection exceeds the 256-job matrix limit.')
    }
    return @{ include = @(foreach ($path in $modules) { @{ modulePath = $path } }) }
}

function Set-AvmBicepReleaseTagsInternal {
    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAlignAssignmentStatement', '',
        Justification = 'Proposal fields preserve the established release result contract.')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseConsistentIndentation', '',
        Justification = 'The nested process environment hashtable is indented for readable argument grouping.')]
    [OutputType([pscustomobject[]])]
    param(
        [Parameter(Mandatory)] [string] $Root,
        [Parameter(Mandatory)] [string] $ModulePath,
        [Parameter(Mandatory)] [string] $TargetCommit,
        [string] $BaseCommit,
        [switch] $Force,
        [switch] $IncludeAllVersionedModules,
        [switch] $PendingRelease,
        [switch] $RequireChangelog
    )

    if ($TargetCommit -cnotmatch '^[0-9a-f]{40}$' -or
        (-not [string]::IsNullOrEmpty($BaseCommit) -and $BaseCommit -cnotmatch '^[0-9a-f]{40}$')) {
        throw [ArgumentException]::new('Release target and base commits must be full SHA-1 identifiers.')
    }
    if ($IncludeAllVersionedModules -and -not $WhatIfPreference) {
        throw [ArgumentException]::new('The missing-base all-module fallback is preview-only.')
    }
    if ($PendingRelease -and ($Force -or $IncludeAllVersionedModules -or $BaseCommit)) {
        throw [ArgumentException]::new('Pending releases cannot override per-module baselines or force unchanged modules.')
    }
    Assert-AvmBicepWorkflowReleaseContext -Root $Root -TargetCommit $TargetCommit
    $module = Resolve-AvmBicepWorkflowPath -Root $Root -RelativePath $ModulePath
    $dirty = Invoke-AvmBicepWorkflowGit -Root $Root -Arguments @('status', '--porcelain', '--untracked-files=all', '--', $ModulePath)
    if (-not [string]::IsNullOrWhiteSpace($dirty.StdOut)) {
        throw [InvalidOperationException]::new('Release module files must be unchanged from the target commit.')
    }
    if (-not $PendingRelease -and [string]::IsNullOrEmpty($BaseCommit)) {
        $parent = Invoke-AvmBicepWorkflowGit -Root $Root -Arguments @('rev-parse', '--verify', "${TargetCommit}^") -AllowFailure
        if ($parent.ExitCode -eq 0) { $BaseCommit = $parent.StdOut.Trim() }
    }
    elseif ($BaseCommit) {
        $available = Invoke-AvmBicepWorkflowGit -Root $Root -Arguments @('cat-file', '-e', "${BaseCommit}^{commit}") -AllowFailure
        if ($available.ExitCode -ne 0) {
            $null = Invoke-AvmBicepWorkflowGit -Root $Root -Arguments @('fetch', '--no-tags', 'origin', $BaseCommit) -Remote
            $null = Invoke-AvmBicepWorkflowGit -Root $Root -Arguments @('cat-file', '-e', "${BaseCommit}^{commit}")
        }
    }
    $candidates = [Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal)
    if ($IncludeAllVersionedModules) {
        foreach ($file in Get-ChildItem -LiteralPath $module -Recurse -File -Filter main.json) {
            if (Test-Path -LiteralPath (Join-Path $file.DirectoryName 'version.json') -PathType Leaf) {
                $null = $candidates.Add([IO.Path]::GetRelativePath($Root, $file.DirectoryName).Replace('\', '/'))
            }
        }
    }
    elseif (-not [string]::IsNullOrEmpty($BaseCommit)) {
        foreach ($relative in Get-AvmBicepWorkflowPublishableChangesInternal -Root $Root -ModulePath $ModulePath `
                -BaseCommit $BaseCommit -TargetCommit $TargetCommit) {
            $null = $candidates.Add($relative)
        }
    }
    if ($Force) {
        foreach ($file in @('main.json', 'version.json')) {
            $null = Resolve-AvmBicepWorkflowPath -Root $Root -RelativePath "$ModulePath/$file"
        }
        $null = $candidates.Add($ModulePath)
    }
    $plan = if ($PendingRelease) {
        @(Get-AvmBicepWorkflowPendingReleasePlanInternal -Root $Root -ModulePath $ModulePath -TargetCommit $TargetCommit)
    }
    else {
        @(foreach ($relative in @($candidates | Sort-Object @{ Expression = { $_.Length }; Descending = $true }, @{ Expression = { $_ } })) {
                [pscustomobject]@{ ModulePath = $relative; BaseCommit = $BaseCommit }
            })
    }
    $proposals = [Collections.Generic.List[object]]::new()
    foreach ($candidate in $plan) {
        $relative = $candidate.ModulePath
        $candidateBase = $candidate.BaseCommit
        $versionPath = Resolve-AvmBicepWorkflowPath -Root $Root -RelativePath "$relative/version.json"
        $version = (ConvertFrom-Json -InputObject (Get-Content -LiteralPath $versionPath -Raw)).version
        if ($version -isnot [string] -or $version -cnotmatch '^\d+\.\d+$') {
            throw [ArgumentException]::new("The version file for $relative must contain MAJOR.MINOR.")
        }
        $remoteTags = Get-AvmBicepWorkflowRemoteTags -Root $Root -Pattern "$relative/$version.*"
        $valid = @($remoteTags.Keys | Where-Object { $_ -cmatch "^$([regex]::Escape("$relative/$version."))\d+$" })
        $existing = @($valid | Where-Object { $remoteTags[$_] -ceq $TargetCommit } |
                Sort-Object { [version]($_.Substring($_.LastIndexOf('/') + 1)) } -Descending)
        $reused = $existing.Count -gt 0
        if ($reused) { $tag = $existing[0] }
        else {
            $changedVersion = $false
            if (-not [string]::IsNullOrEmpty($candidateBase)) {
                $previous = Invoke-AvmBicepWorkflowGit -Root $Root -Arguments @('show', "${candidateBase}:$relative/version.json") -AllowFailure
                $changedVersion = $previous.ExitCode -ne 0 -or (ConvertFrom-Json -InputObject $previous.StdOut).version -cne $version
            }
            $patch = 0
            if (-not $changedVersion -and $valid.Count -gt 0) {
                $patch = 1 + ($valid | ForEach-Object { [int]$_.Substring($_.LastIndexOf('.') + 1) } |
                        Measure-Object -Maximum).Maximum
            }
            $tag = "$relative/$version.$patch"
        }
        $null = Invoke-AvmBicepWorkflowGit -Root $Root -Arguments @('check-ref-format', "refs/tags/$tag")
        if ($remoteTags.ContainsKey($tag) -and $remoteTags[$tag] -cne $TargetCommit) {
            throw [InvalidOperationException]::new("Release tag $tag already points to a different commit.")
        }
        $proposals.Add([pscustomobject]@{ Module = $relative; Tag = $tag; Commit = $TargetCommit; Reused = $reused; Created = $false })
    }
    if ($RequireChangelog) {
        foreach ($proposal in $proposals) {
            Assert-AvmBicepWorkflowReleaseChangelog -Root $Root -ModulePath $proposal.Module `
                -Version $proposal.Tag.Substring($proposal.Module.Length + 1)
        }
    }
    foreach ($proposal in $proposals) {
        if ($proposal.Reused -or -not $PSCmdlet.ShouldProcess("$($proposal.Tag) at $TargetCommit", 'Create and push module release tag')) {
            continue
        }
        if ([string]::IsNullOrWhiteSpace($env:GITHUB_TOKEN)) {
            throw [InvalidOperationException]::new('The release-tag job requires its scoped GitHub token.')
        }
        $local = Invoke-AvmBicepWorkflowGit -Root $Root -Arguments @('rev-parse', '-q', '--verify', "refs/tags/$($proposal.Tag)^{}") -AllowFailure
        if ($local.ExitCode -eq 0 -and $local.StdOut.Trim() -cne $TargetCommit) {
            throw [InvalidOperationException]::new("Local tag $($proposal.Tag) points to a different commit.")
        }
        $git = (Get-Command -Name git -CommandType Application -ErrorAction Stop | Select-Object -First 1).Source
        if ($local.ExitCode -ne 0) {
            $null = Invoke-AvmProcess -FilePath $git -WorkingDirectory $Root -ArgumentList @('tag', $proposal.Tag, $TargetCommit)
        }
        $authorization = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes("x-access-token:$env:GITHUB_TOKEN"))
        $confirmed = $false
        $pushEnvironment = @{
            GIT_TERMINAL_PROMPT = '0'
            GIT_CONFIG_COUNT = '1'
            GIT_CONFIG_KEY_0 = 'http.https://github.com/.extraheader'
            GIT_CONFIG_VALUE_0 = "AUTHORIZATION: basic $authorization"
        }
        try {
            $push = Invoke-AvmProcess -FilePath $git -WorkingDirectory $Root `
                -ArgumentList @('push', 'origin', "refs/tags/$($proposal.Tag):refs/tags/$($proposal.Tag)") -IgnoreExitCode `
                -EnvVars $pushEnvironment
            $confirmed = $push.ExitCode -eq 0
        }
        catch [TimeoutException] {
            Write-Warning "Publishing $($proposal.Tag) timed out; checking the remote result without replaying the push."
        }
        if (-not $confirmed) {
            $reconciled = Get-AvmBicepWorkflowRemoteTags -Root $Root -Pattern "$($proposal.Tag)*"
            if (-not $reconciled.ContainsKey($proposal.Tag) -or $reconciled[$proposal.Tag] -cne $TargetCommit) {
                throw [InvalidOperationException]::new("Publishing $($proposal.Tag) failed; no mutation was retried.")
            }
        }
        $proposal.Created = $true
    }
    return $proposals.ToArray()
}

function Get-AvmBicepWorkflowModuleIdentityInternal {
    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAlignAssignmentStatement', '',
        Justification = 'Identity field names preserve the public E2E context contract.')]
    [OutputType([pscustomobject])]
    param(
        [Parameter(Mandatory)] [string] $Root,
        [Parameter(Mandatory)] [string] $ModulePath,
        [Parameter(Mandatory)] [AllowEmptyString()] [string] $ModuleClientIds,
        [Parameter(Mandatory)] [AllowEmptyString()] [string] $TenantId
    )

    if ([string]::IsNullOrWhiteSpace($ModuleClientIds)) {
        throw [ArgumentException]::new('ModuleClientIds must contain the published module client-ID mapping; no shared identity fallback is supported.')
    }
    try {
        $mapping = ConvertFrom-AvmStrictJson -Json $ModuleClientIds -RejectCaseInsensitiveDuplicates
    }
    catch [System.Exception] {
        throw [ArgumentException]::new('ModuleClientIds is invalid; require unique canonical root paths and distinct nonempty client GUIDs.', $_.Exception)
    }
    if ($mapping -isnot [Collections.IDictionary]) {
        throw [ArgumentException]::new('ModuleClientIds must be a JSON object.')
    }
    $clients = [Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
    foreach ($key in $mapping.Keys) {
        $id = [guid]::Empty
        if ($key -cnotmatch '^avm/(?:res|ptn|utl)/[a-z0-9-]+/[a-z0-9-]+$' -or
            $mapping[$key] -isnot [string] -or -not [guid]::TryParseExact($mapping[$key], 'D', [ref]$id) -or
            $id -eq [guid]::Empty -or -not $clients.Add($id.ToString('D'))) {
            throw [ArgumentException]::new('ModuleClientIds is invalid; require unique canonical root paths and distinct nonempty client GUIDs.')
        }
    }
    $path = Resolve-AvmBicepWorkflowPath -Root $Root -RelativePath $ModulePath
    $relative = $ModulePath.Replace('\', '/')
    if ([IO.Path]::GetRelativePath([IO.Path]::GetFullPath($Root), $path).Replace('\', '/') -cne $relative) {
        throw [ArgumentException]::new('The module path must be canonical without empty or current-directory segments.')
    }
    $roots = @($mapping.Keys | Where-Object { $relative -ceq $_ -or $relative.StartsWith("$_/", [StringComparison]::Ordinal) })
    if ($roots.Count -ne 1) {
        throw [ArgumentException]::new("ModuleClientIds must contain exactly one canonical root for '$relative'.")
    }
    $tenant = [guid]::Empty
    if (-not [guid]::TryParseExact($TenantId, 'D', [ref]$tenant) -or $tenant -eq [guid]::Empty) {
        throw [ArgumentException]::new('TenantId must be the nonempty GUID published with the module identity mapping.')
    }
    $client = ([guid]$mapping[$roots[0]]).ToString('D')
    return [pscustomobject]@{
        ModuleRoot = $roots[0]
        ClientId = $client
        TenantId = $tenant.ToString('D')
        IdentityKey = [Convert]::ToHexString([Security.Cryptography.SHA256]::HashData(
                [Text.Encoding]::UTF8.GetBytes("$($roots[0])|$client|$($tenant.ToString('D'))"))).ToLowerInvariant()
    }
}

function Select-AvmBicepWorkflowDeploymentSubscriptionInternal {
    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param(
        [Parameter(Mandatory)] [string] $Root,
        [Parameter(Mandatory)] [string] $ModulePath,
        [Parameter(Mandatory)] [string] $Example,
        [AllowEmptyString()] [string] $PoolJson,
        [AllowEmptyString()] [string] $FallbackSubscriptionId,
        [ValidateRange(0, [int]::MaxValue)] [int] $RandomSeed = 0,
        [ValidateRange(0, [int]::MaxValue)] [int] $CaseIndex = 0
    )

    if ([string]::IsNullOrEmpty($PoolJson)) {
        $PoolJson = ConvertTo-Json -InputObject @(@{ id = $FallbackSubscriptionId; name = 'Configured subscription 1' }) -Compress
    }
    $selected = @(Select-AvmTestSubscription -Path $Root -ModulePath $ModulePath -Example $Example `
            -TestSubscriptionIds $PoolJson -RandomSeed $RandomSeed -CaseIndex $CaseIndex -SkipModuleVersionCheck)
    if ($selected.Count -ne 1) {
        throw [InvalidOperationException]::new('Native subscription selection must return exactly one eligible target.')
    }
    if ($selected[0].Tag -cne 'General') {
        throw [InvalidOperationException]::new('Reserved test subscriptions require the registry-local prepared environment lifecycle; shared native deployment cannot authenticate or execute this case.')
    }
    $ownerPath = Resolve-AvmBicepWorkflowPath -Root $Root -RelativePath $selected[0].ModulePath
    $null = Get-AvmModuleContext -Path $ownerPath -Ecosystem bicep -SkipModuleVersionCheck
    return $selected[0]
}

function Get-AvmBicepWorkflowCompiledTestInternal {
    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory)] [string] $Root,
        [Parameter(Mandatory)] [string] $ModulePath,
        [Parameter(Mandatory)] [string] $TestPath,
        [scriptblock] $ToolResolver,
        [scriptblock] $ToolInstaller,
        [scriptblock] $ProcessInvoker
    )

    $module = Resolve-AvmBicepWorkflowPath -Root $Root -RelativePath $ModulePath
    $tool = if ($null -ne $ToolResolver) {
        & $ToolResolver -Name bicep -Path $module
    }
    else {
        Get-AvmTool -Name bicep -Path $module -SkipModuleVersionCheck
    }
    if ($tool.Status -ceq 'not-installed') {
        if ($null -ne $ToolInstaller) {
            $null = & $ToolInstaller -Name bicep -Path $module
        }
        else {
            $null = Install-AvmTool -Name bicep -Path $module -SkipModuleVersionCheck -Confirm:$false
        }
        $tool = if ($null -ne $ToolResolver) {
            & $ToolResolver -Name bicep -Path $module
        }
        else {
            Get-AvmTool -Name bicep -Path $module -SkipModuleVersionCheck
        }
    }
    if ($tool.Status -cne 'installed') { throw [InvalidOperationException]::new('The managed Bicep compiler is not installed.') }
    $path = Resolve-AvmBicepWorkflowPath -Root $Root -RelativePath "$ModulePath/$TestPath"
    $result = if ($null -ne $ProcessInvoker) {
        & $ProcessInvoker -FilePath $tool.Path -ArgumentList @('build', $path, '--stdout') -WorkingDirectory $Root
    }
    else {
        Invoke-AvmProcess -FilePath $tool.Path -ArgumentList @('build', $path, '--stdout') -WorkingDirectory $Root
    }
    $template = ConvertFrom-Json -InputObject $result.StdOut -AsHashtable
    if ($template -isnot [Collections.IDictionary]) { throw [ArgumentException]::new('The compiled test must be an ARM template object.') }
    return [string]$result.StdOut
}

function Get-AvmBicepWorkflowIsolationInternal {
    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAlignAssignmentStatement', '',
        Justification = 'Isolation fields preserve the public E2E context contract.')]
    [OutputType([pscustomobject])]
    param(
        [Parameter(Mandatory)] [string] $Root,
        [Parameter(Mandatory)] [string] $ModulePath,
        [Parameter(Mandatory)] [string] $TestPath,
        [Parameter(Mandatory)] [string] $TenantId,
        [Parameter(Mandatory)] [string] $SubscriptionId,
        [Parameter(Mandatory)] [string] $CompiledTemplate,
        [Parameter(Mandatory)] [string] $FallbackGroup
    )

    $casePath = Resolve-AvmBicepWorkflowPath -Root $Root -RelativePath "$ModulePath/$TestPath"
    $directory = Split-Path -Path $casePath -Parent
    $fallback = [pscustomobject]@{
        IsolationMode = 'fallback'
        IsolationBoundary = ''
        ConcurrencyGroup = $FallbackGroup
        FallbackModuleLock = Test-AvmBicepWorkflowLegacyModuleLockInternal -Template (ConvertFrom-Json -InputObject $CompiledTemplate -AsHashtable)
        IsolationDiagnostic = 'Isolation is not established: no case-local isolation.json. Retaining legacy fallback serialization, not claiming ownership.'
    }
    $manifestPath = Join-Path $directory 'isolation.json'
    if (-not (Test-Path -LiteralPath $manifestPath)) { return $fallback }
    try {
        $contract = ConvertFrom-Json -InputObject ([IO.File]::ReadAllText($manifestPath, [Text.UTF8Encoding]::new($false, $true))) -AsHashtable
    }
    catch [System.Exception] {
        throw [ArgumentException]::new('Isolation evidence must be valid UTF-8 JSON.', $_.Exception)
    }
    if ($contract -isnot [Collections.IDictionary] -or
        @($contract.Keys | Where-Object { $_ -cnotin @('version', 'evidence', 'sharedTarget') }).Count -gt 0 -or
        ($contract['version'] -isnot [long] -and $contract['version'] -isnot [int]) -or $contract['version'] -ne 1 -or
        $contract['evidence'] -isnot [Collections.IDictionary] -or
        @($contract['evidence'].Keys | Where-Object { $_ -notin @('resources', 'cleanup', 'sharedState', 'identity') }).Count -gt 0 -or
        $contract['evidence'].Count -ne 4) {
        throw [ArgumentException]::new('Isolation requires version 1 and four reviewed ownership evidence fields.')
    }
    foreach ($name in @('resources', 'cleanup', 'sharedState', 'identity')) {
        if ($contract['evidence'][$name] -isnot [string] -or [string]::IsNullOrWhiteSpace($contract['evidence'][$name])) {
            throw [ArgumentException]::new("Isolation evidence must explain '$name'.")
        }
    }
    $template = ConvertFrom-Json -InputObject $CompiledTemplate -AsHashtable
    try {
        $boundary = Assert-AvmBicepResourceIsolationTemplate -Template $template
    }
    catch [AvmConfigurationException] {
        $fallback.IsolationDiagnostic = "Isolation ownership is not established: $($_.Exception.Message) Retaining legacy fallback serialization."
        return $fallback
    }
    if (-not $boundary -or (Test-Path -LiteralPath (Join-Path $directory 'post.ps1'))) {
        $fallback.IsolationDiagnostic = 'Isolation ownership is not established. Retaining legacy fallback serialization.'
        return $fallback
    }
    $target = if ($contract.Contains('sharedTarget')) { $contract['sharedTarget'] } else { $null }
    if ($null -ne $target -and ($target -isnot [string] -or
            $target -notmatch '^/(?:subscriptions/|providers/)[A-Za-z0-9_()./#-]+$' -or $target.Contains('//'))) {
        throw [ArgumentException]::new('sharedTarget must identify one absolute ARM resource or singleton setting.')
    }
    if ($null -ne $target) {
        $target = $target.Replace('#_subscriptionId_#', ([guid]$SubscriptionId).ToString('D'))
        if ($target.Contains('#')) { throw [ArgumentException]::new('sharedTarget contains an unsupported token.') }
        $hash = [Convert]::ToHexString([Security.Cryptography.SHA256]::HashData(
                [Text.Encoding]::UTF8.GetBytes(("azurecloud|$TenantId|$target").ToLowerInvariant()))).ToLowerInvariant()
        return [pscustomobject]@{
            IsolationMode = 'shared'; IsolationBoundary = $boundary; ConcurrencyGroup = "avm-deploy-target-$hash"
            FallbackModuleLock = $false
            IsolationDiagnostic = 'Reviewed isolation with an explicit shared-target lifecycle lock.'
        }
    }
    return [pscustomobject]@{
        IsolationMode = 'isolated'; IsolationBoundary = $boundary
        ConcurrencyGroup = 'avm-deploy-isolated-' + [guid]::NewGuid().ToString('N')
        FallbackModuleLock = $false
        IsolationDiagnostic = 'Reviewed resource and cleanup isolation: no serialization lock required.'
    }
}

function Get-AvmBicepE2eDeploymentMatrixInternal {
    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAlignAssignmentStatement', '',
        Justification = 'Matrix field names preserve the public E2E context contract.')]
    [OutputType([pscustomobject[]])]
    param(
        [Parameter(Mandatory)] [string] $Root,
        [Parameter(Mandatory)] [string] $ModulePath,
        [Parameter(Mandatory)] [AllowEmptyCollection()] [object[]] $TestFiles,
        [AllowEmptyString()] [string] $PoolJson,
        [AllowEmptyString()] [string] $FallbackSubscriptionId,
        [AllowEmptyString()] [string] $ModuleClientIds,
        [AllowEmptyString()] [string] $TenantId,
        [ValidateRange(0, [int]::MaxValue)] [int] $RandomSeed = 0,
        [switch] $DisplaySubscriptionNames,
        [scriptblock] $ToolResolver,
        [scriptblock] $ToolInstaller,
        [scriptblock] $ProcessInvoker
    )

    $active = @($TestFiles | Where-Object { -not (ConvertTo-AvmBicepWorkflowBooleanInternal -Value $_.e2eIgnore) })
    $identity = if ($active.Count -gt 0) {
        Get-AvmBicepWorkflowModuleIdentityInternal -Root $Root -ModulePath $ModulePath -ModuleClientIds $ModuleClientIds -TenantId $TenantId
    }
    $moduleKey = $ModulePath.Replace('\', '/').Trim('/').ToLowerInvariant()
    $selections = @{}
    for ($index = 0; $index -lt $TestFiles.Count; $index++) {
        $test = $TestFiles[$index]
        if (-not (ConvertTo-AvmBicepWorkflowBooleanInternal -Value $test.e2eIgnore)) {
            $selections[$index] = Select-AvmBicepWorkflowDeploymentSubscriptionInternal -Root $Root -ModulePath $ModulePath `
                -Example $test.path -PoolJson $PoolJson -FallbackSubscriptionId $FallbackSubscriptionId `
                -RandomSeed $RandomSeed -CaseIndex $index
        }
    }
    $result = [Collections.Generic.List[object]]::new()
    for ($index = 0; $index -lt $TestFiles.Count; $index++) {
        $test = $TestFiles[$index]
        if ($test.path -isnot [string] -or $test.name -isnot [string] -or [string]::IsNullOrWhiteSpace($test.name)) {
            throw [ArgumentException]::new('Every deployment test must contain a path and a single-line name.')
        }
        $ignored = ConvertTo-AvmBicepWorkflowBooleanInternal -Value $test.e2eIgnore
        if ($ignored) {
            $result.Add([pscustomobject]@{
                    Path = $test.path; Name = $test.name; E2eIgnore = $true; SubscriptionIndex = $null; SubscriptionKey = ''
                    SelectionSeed = $null; CaseIndex = $index; SubscriptionModulePath = ''; IdentityKey = ''
                    SubscriptionName = 'Deployment disabled'; ConcurrencyGroup = "avm-deploy-$moduleKey-ignored-$([guid]::NewGuid().ToString('N'))"
                    IsolationMode = 'ignored'; IsolationBoundary = ''; IsolationDiagnostic = 'Deployment disabled.'
                    FallbackModuleLock = $false
                })
            continue
        }
        $selected = $selections[$index]
        $compiled = Get-AvmBicepWorkflowCompiledTestInternal -Root $Root -ModulePath $ModulePath -TestPath $test.path `
            -ToolResolver $ToolResolver -ToolInstaller $ToolInstaller -ProcessInvoker $ProcessInvoker
        $isolation = Get-AvmBicepWorkflowIsolationInternal -Root $Root -ModulePath $ModulePath -TestPath $test.path `
            -TenantId $identity.TenantId -SubscriptionId $selected.SubscriptionId -CompiledTemplate $compiled `
            -FallbackGroup "avm-deploy-$moduleKey-$($selected.Key)"
        $result.Add([pscustomobject]@{
                Path = $test.path; Name = $test.name; E2eIgnore = $false; SubscriptionIndex = $selected.Index; SubscriptionKey = $selected.Key
                SelectionSeed = $RandomSeed; CaseIndex = $index; SubscriptionModulePath = $selected.ModulePath; IdentityKey = $identity.IdentityKey
                SubscriptionName = if ($DisplaySubscriptionNames) { $selected.Name } else { "Configured subscription $($selected.Index + 1)" }
                IsolationMode = $isolation.IsolationMode; ConcurrencyGroup = $isolation.ConcurrencyGroup
                IsolationBoundary = $isolation.IsolationBoundary; IsolationDiagnostic = $isolation.IsolationDiagnostic
                FallbackModuleLock = $isolation.FallbackModuleLock
            })
    }
    $legacyModuleLock = @($result | Where-Object { $_.IsolationMode -eq 'fallback' -and $_.FallbackModuleLock }).Count -gt 0
    foreach ($entry in $result) {
        if ($entry.IsolationMode -eq 'fallback') { $entry.FallbackModuleLock = $legacyModuleLock }
    }
    return $result.ToArray()
}

function Get-AvmBicepWorkflowTestCaseValueInternal {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [object] $TestCase,
        [Parameter(Mandatory)] [string] $Name,
        [switch] $Optional
    )

    if ($TestCase -is [Collections.IDictionary]) {
        $key = @($TestCase.Keys | Where-Object { $_ -ieq $Name } | Select-Object -First 1)
        if ($key.Count -eq 1) { return $TestCase[$key[0]] }
        if ($Optional) { return $null }
        throw [ArgumentException]::new("The selected test case does not contain '$Name'. Rerun selection.")
    }
    $property = $TestCase.PSObject.Properties[$Name]
    if ($null -eq $property -and $Optional) { return $null }
    if ($null -eq $property) {
        throw [ArgumentException]::new("The selected test case does not contain '$Name'. Rerun selection.")
    }
    return $property.Value
}

function Get-AvmBicepWorkflowSubscriptionCleanupManagementGroupInternal {
    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory)] [string] $ModuleRoot,
        [AllowEmptyString()] [string] $ModuleSubscriptionCleanupManagementGroupIds
    )

    if ([string]::IsNullOrWhiteSpace($ModuleSubscriptionCleanupManagementGroupIds)) { return '' }
    try {
        $mapping = ConvertFrom-AvmStrictJson -Json $ModuleSubscriptionCleanupManagementGroupIds -RejectCaseInsensitiveDuplicates
    }
    catch [System.Exception] {
        throw [ArgumentException]::new('Module subscription cleanup management groups must be valid JSON.', $_.Exception)
    }
    $root = 'avm/ptn/lz/sub-vending'
    if ($mapping -isnot [Collections.IDictionary] -or $mapping.Count -gt 1 -or
        ($mapping.Count -eq 1 -and -not $mapping.Contains($root)) -or
        ($mapping.Contains($root) -and (
            $mapping[$root] -isnot [string] -or
            $mapping[$root] -cnotmatch '^[A-Za-z0-9_().-]{1,90}$' -or
            $mapping[$root] -in @('.', '..') -or $mapping[$root].EndsWith('.')))) {
        throw [ArgumentException]::new('Module subscription cleanup management groups must contain only one valid canonical target.')
    }
    if ($ModuleRoot -ceq $root) { return [string]$mapping[$root] }
    return ''
}

function Get-AvmBicepE2eTestContextInternal {
    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param(
        [Parameter(Mandatory)] [string] $Root,
        [Parameter(Mandatory)] [string] $ModulePath,
        [Parameter(Mandatory)] [object] $TestCase,
        [AllowEmptyString()] [string] $PoolJson,
        [AllowEmptyString()] [string] $FallbackSubscriptionId,
        [Parameter(Mandatory)] [AllowEmptyString()] [string] $ModuleClientIds,
        [Parameter(Mandatory)] [AllowEmptyString()] [string] $TenantId,
        [AllowEmptyString()] [string] $ModuleSubscriptionCleanupManagementGroupIds,
        [AllowEmptyString()] [string] $NamePrefix,
        [scriptblock] $ToolResolver,
        [scriptblock] $ToolInstaller,
        [scriptblock] $ProcessInvoker
    )

    $identity = Get-AvmBicepWorkflowModuleIdentityInternal -Root $Root -ModulePath $ModulePath -ModuleClientIds $ModuleClientIds -TenantId $TenantId
    $subscriptionCleanupManagementGroupId = Get-AvmBicepWorkflowSubscriptionCleanupManagementGroupInternal `
        -ModuleRoot $identity.ModuleRoot -ModuleSubscriptionCleanupManagementGroupIds $ModuleSubscriptionCleanupManagementGroupIds
    $caseIdentityKey = Get-AvmBicepWorkflowTestCaseValueInternal -TestCase $TestCase -Name IdentityKey -Optional
    if ($caseIdentityKey -cne $identity.IdentityKey) { throw [ArgumentException]::new('The selected module identity no longer matches its root, client and tenant. Rerun selection.') }
    $seed = [int](Get-AvmBicepWorkflowTestCaseValueInternal -TestCase $TestCase -Name SelectionSeed)
    $index = [int](Get-AvmBicepWorkflowTestCaseValueInternal -TestCase $TestCase -Name CaseIndex)
    $path = Get-AvmBicepWorkflowTestCaseValueInternal -TestCase $TestCase -Name Path
    $selected = Select-AvmBicepWorkflowDeploymentSubscriptionInternal -Root $Root -ModulePath $ModulePath -Example $path `
        -PoolJson $PoolJson -FallbackSubscriptionId $FallbackSubscriptionId -RandomSeed $seed -CaseIndex $index
    $caseSubscriptionIndex = Get-AvmBicepWorkflowTestCaseValueInternal -TestCase $TestCase -Name SubscriptionIndex -Optional
    $caseSubscriptionKey = Get-AvmBicepWorkflowTestCaseValueInternal -TestCase $TestCase -Name SubscriptionKey
    $caseSubscriptionModulePath = Get-AvmBicepWorkflowTestCaseValueInternal -TestCase $TestCase -Name SubscriptionModulePath
    if ($null -eq $caseSubscriptionIndex -or $selected.Index -ne [int]$caseSubscriptionIndex -or $selected.Key -cne $caseSubscriptionKey -or
        $selected.ModulePath -cne $caseSubscriptionModulePath) {
        throw [ArgumentException]::new('The selected subscription no longer matches the original pool index and digest. Rerun selection.')
    }
    $testPath = Resolve-AvmBicepWorkflowPath -Root $Root -RelativePath "$ModulePath/$path"
    $ownerPath = Resolve-AvmBicepWorkflowPath -Root $Root -RelativePath $selected.ModulePath
    $example = [IO.Path]::GetRelativePath($ownerPath, $testPath).Replace('\', '/')
    $compiled = Get-AvmBicepWorkflowCompiledTestInternal -Root $Root -ModulePath $ModulePath -TestPath $path `
        -ToolResolver $ToolResolver -ToolInstaller $ToolInstaller -ProcessInvoker $ProcessInvoker
    $caseConcurrencyGroup = Get-AvmBicepWorkflowTestCaseValueInternal -TestCase $TestCase -Name ConcurrencyGroup
    $caseIsolationMode = Get-AvmBicepWorkflowTestCaseValueInternal -TestCase $TestCase -Name IsolationMode
    $caseFallbackModuleLock = Get-AvmBicepWorkflowTestCaseValueInternal -TestCase $TestCase -Name FallbackModuleLock -Optional
    $moduleKey = $ModulePath.Replace('\', '/').Trim('/').ToLowerInvariant()
    $fallbackGroup = "avm-deploy-$moduleKey-$($selected.Key)"
    $isolation = Get-AvmBicepWorkflowIsolationInternal -Root $Root -ModulePath $ModulePath -TestPath $path `
        -TenantId $identity.TenantId -SubscriptionId $selected.SubscriptionId -CompiledTemplate $compiled `
        -FallbackGroup $fallbackGroup
    if ($isolation.IsolationMode -cne $caseIsolationMode -or
        ($isolation.IsolationMode -ne 'isolated' -and $isolation.ConcurrencyGroup -cne $caseConcurrencyGroup) -or
        ($isolation.IsolationMode -eq 'fallback' -and $isolation.FallbackModuleLock -and $caseFallbackModuleLock -ne $true) -or
        ($isolation.IsolationMode -eq 'isolated' -and $caseConcurrencyGroup -cnotmatch '^avm-deploy-isolated-[0-9a-f]{32}$')) {
        throw [ArgumentException]::new('Isolation evidence or lifecycle lock changed after selection. Rerun selection before authentication.')
    }
    $prefix = Get-AvmBicepWorkflowNamePrefixInternal -IsolationMode $isolation.IsolationMode -Prefix $NamePrefix
    return [pscustomobject]@{
        SubscriptionId = $selected.SubscriptionId; TenantId = $identity.TenantId; ClientId = $identity.ClientId
        ModulePath = $selected.ModulePath; Example = $example; NamePrefix = $prefix
        ArtifactKey = "bicep-e2e-$($selected.Key)-$index"; IsolationMode = $isolation.IsolationMode
        ConcurrencyGroup = $isolation.ConcurrencyGroup; RequireResourceIsolation = $isolation.IsolationMode -ne 'fallback'
        SubscriptionCleanupManagementGroupId = $subscriptionCleanupManagementGroupId
    }
}

# SIG # Begin signature block
# MIInQQYJKoZIhvcNAQcCoIInMjCCJy4CAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCATI8afLy/sXCkl
# FOp0tFthQ8sBTRC6pvidnc4WP+L22qCCDLowggX1MIID3aADAgECAhMzAAACHU0Z
# yE7XD1dIAAAAAAIdMA0GCSqGSIb3DQEBCwUAMFcxCzAJBgNVBAYTAlVTMR4wHAYD
# VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBD
# b2RlIFNpZ25pbmcgUENBIDIwMjQwHhcNMjYwNDE2MTg1OTQzWhcNMjcwNDE1MTg1
# OTQzWjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE
# BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYD
# VQQDExVNaWNyb3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IB
# DwAwggEKAoIBAQDQvewXxx9gZZFC6Ys1WBay8BJ8kGA4JQnH5CMafqOASlTpK9H8
# o5ZXTXt0caVQTNMUPt445wXYD+dFtaKWTwDn1I52oUSrC9vJin1Gsqt+zyKJL5Dg
# 3eQXbQNR61DmMy20GLTIO3SFed9Rfi/ophgCLGFLDR3r0KvHjwMb/jYWS0celV/4
# Lz27LfAekm8v9E5IXaeiXbAUYZKK090n4CVl3JBtbN+9DtI9SNu/yjvozW52/u7R
# X/Ttpa/KDlpuokZ+Zcbvmtd9ur9gFLvZzh41o9MsE/clQtdaFWGvuo6Jua/ntpgk
# ey3E5/vBFe+MJPG6phdnuo6r57ZudCudiI1bAgMBAAGjggGbMIIBlzAOBgNVHQ8B
# Af8EBAMCB4AwHwYDVR0lBBgwFgYKKwYBBAGCN0wIAQYIKwYBBQUHAwMwHQYDVR0O
# BBYEFH6QuMwqcPG0hQlQ6c5jCtTTLrVeMEUGA1UdEQQ+MDykOjA4MR4wHAYDVQQL
# ExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xFjAUBgNVBAUTDTIzMDAxMis1MDc1NTkw
# HwYDVR0jBBgwFoAUf1k/VCHarU/vBeXmo9ctBpQSCDEwYAYDVR0fBFkwVzBVoFOg
# UYZPaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jcmwvTWljcm9zb2Z0
# JTIwQ29kZSUyMFNpZ25pbmclMjBQQ0ElMjAyMDI0LmNybDBtBggrBgEFBQcBAQRh
# MF8wXQYIKwYBBQUHMAKGUWh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMv
# Y2VydHMvTWljcm9zb2Z0JTIwQ29kZSUyMFNpZ25pbmclMjBQQ0ElMjAyMDI0LmNy
# dDAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4ICAQBKTbYOjzwTG/DXGaz9
# s6+fQeaTtDcFmMY+5UyVFCyj7Pv+5i37qfX8lSL/tBIfYQfWsMuBQlfZurJD6r4H
# VJ2CeH+1fgiq8dcHdVKoZ3Sa2qXoX3cq9iS8cVb06B7+5/XJ7I0OxHH9fDsvJ3T3
# w5V/ZtAIFmLrl+P0CtG+92uzRsn0nTbdFjOkLMLWPLAU3THohKRlSEMgFJpPkm5n
# 5UAZ35xX6FWCrDLsSKb555bTifwa8mJBwdlof0bmfYidH+dxZ1FdDxvLnNl9zeKs
# A4kejaaIqqIPguhwAti5Ql7BlTNoJNwxCvBmqW2MQLnCkYN/VVUsR3V2x/rcTNzo
# Bf/Z/SpROvdaA2ZOOd1uioXJt3tdLQ7vHpqpib0KfWr/FWXW10q38VxfCnRQBqzb
# SuztR7nEMuzX7Ck+B/XaPDXd1qh72+QYyB0Z2VzWmO9zsnb9Uq/dwu8LGeQqnyu6
# 7SDGACvnXii2fb9+US492VTnXSnFKyqwgzUyFMtZK1/sHYTv6bG4TtQUygQxTN+Z
# V+aJIlKO2MqZ7bKrAnOzS9m6NgoTdWOq11bTOZwKlIEV/EhV9SWkDmdpR/hPPT2v
# 6TEj4F8PT/zHjRezIU5c/DGlt/VhY/pK0XkJtEyMmmS1BMtjU/rqBZVMIm3dnxQs
# /TBByr+Cf8Z1r7aifQVQ+WSqzjCCBr0wggSloAMCAQICEzMAAAA5O7Y3Gb8GHWcA
# AAAAADkwDQYJKoZIhvcNAQEMBQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpX
# YXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQg
# Q29ycG9yYXRpb24xMjAwBgNVBAMTKU1pY3Jvc29mdCBSb290IENlcnRpZmljYXRl
# IEF1dGhvcml0eSAyMDExMB4XDTI0MDgwODIwNTQxOFoXDTM2MDMyMjIyMTMwNFow
# VzELMAkGA1UEBhMCVVMxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEo
# MCYGA1UEAxMfTWljcm9zb2Z0IENvZGUgU2lnbmluZyBQQ0EgMjAyNDCCAiIwDQYJ
# KoZIhvcNAQEBBQADggIPADCCAgoCggIBANgBnB7jOMeqlRYHNa265v4IY9fH8TKh
# emHfPINe1gpLaV3dhg324WwH06LcHbpnsBukCDNitryo0dtS/EW6I/yEL/bLSY8h
# KpbfQuWusBPr9qazYcDxCW/qnjb5JsI1s8bNOg3bVATvQVL4tcf03aTycsz8QeCd
# M0l/yHRObJ9QqazM1r6VPEOJ7LL+uEEb73w6QCuhs89a1uv1zerOYMnsneRRwCbp
# yW11IcggU0cRKDDq1pjVJzIbIF6+oiXXbReOsgeI8zu1FyQfK0fVkaya8SmVHQ/t
# Of23mZ4W9k0Ri22QW9p3UgSC5OUDktKxxcCmGL6tXLfOGSWHIIV4YrTJTT6PNty5
# REojHJuZHArkF9VnHTERWoTjAzfI3kP+5b4alUdhgAZ7ttOu1bVnXfHaqPYl2rPs
# 20ji03LOVWsh/radgE17es5hL+t6lV0eVHrVhsssROWJuz2MXMCt7iw7lFPG9LXK
# Gjsmonn2gotGdHIuEg5JnJMJVmixd5LRlkmgYRZKzhxSCwyoGIq0PhaA7Y+VPct5
# pCHkijcIIDm0nlkK+0KyepolcqGm0T/GYQRMhHJlGOOmVQop36wUVUYklUy++vDW
# eEgEo4s7hxN6mIbf2MSIQ/iIfMZgJxC69oukMUXCrOC3SkE/xIkgpfl22MM1itkZ
# 35nNXkMolU1lAgMBAAGjggFOMIIBSjAOBgNVHQ8BAf8EBAMCAYYwEAYJKwYBBAGC
# NxUBBAMCAQAwHQYDVR0OBBYEFH9ZP1Qh2q1P7wXl5qPXLQaUEggxMBkGCSsGAQQB
# gjcUAgQMHgoAUwB1AGIAQwBBMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU
# ci06AjGQQ7kUBU7h6qfHMdEjiTQwWgYDVR0fBFMwUTBPoE2gS4ZJaHR0cDovL2Ny
# bC5taWNyb3NvZnQuY29tL3BraS9jcmwvcHJvZHVjdHMvTWljUm9vQ2VyQXV0MjAx
# MV8yMDExXzAzXzIyLmNybDBeBggrBgEFBQcBAQRSMFAwTgYIKwYBBQUHMAKGQmh0
# dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMvTWljUm9vQ2VyQXV0MjAx
# MV8yMDExXzAzXzIyLmNydDANBgkqhkiG9w0BAQwFAAOCAgEAFJQfOChP7onn6fLI
# MKrSlN1WYKwDFgAddymOUO3FrM8d7B/W/iQ6DxXsDn7D5W4wMwYeLystcEqfkjz4
# NURRgazyMu5yRzQh4LqjA4tStTcJh1opExo7nn5PuPBYnbu0+THSuVHTe0VTTPVh
# ily/piFrDo3axQ9P4C+Ol5yet+2gTfekICS5xS+cYfSIvgn0JksVBVMYVI5QFu/q
# hnLhsEFEUzG8fvv0hjgkO+lkpV9ty6GkN4vdnd7ya6Q6aR9y34aiM1qmxaxBi6OU
# nyNl6fkuun/diTFnYDLTppOkr/mg5WSfCiDVMNCxtj4wPKC5OmHm1DQIt/MNokbb
# H3UGsFP1QbzsLocuSqLCvH09Io3fDPTmscR9Y75G4qX7RTX8AdBPo0I6OEojf39z
# uFZt0qOHm65YWQE69cZM2ueE1MB05dNNgHK9gTE7zKvK/fg8B2qjW88MT/WF5V5u
# vZGtqa9FSL2RazArA+rDPuf6JGYz4HpgMZHB4S6szWSKYBv0VisCzfxgeU+dquXW
# 9bd0auYlOB58DPcOYKdc3Se94g+xL4pcEhbB54JOgAkwYTu/9dLeH2pDqeJZAABV
# DWRQCaXfO5LgyKwKCLYXpigrZYCjUSBcr+Ve8PFWMhVTQl0v4q8J/AUmQN5W4n10
# 1cY2L4A7GTQG1h32HHAvfQESWP0xghndMIIZ2QIBATBuMFcxCzAJBgNVBAYTAlVT
# MR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jv
# c29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMjQCEzMAAAIdTRnITtcPV0gAAAAAAh0w
# DQYJYIZIAWUDBAIBBQCggZAwGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwLwYJ
# KoZIhvcNAQkEMSIEIPpy4K/UOXDeUmz2JUneD2XGYEm0EbpVrTHt/6emIyu+MEIG
# CisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8AcwBvAGYAdKEagBhodHRwOi8v
# d3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEBBQAEggEAVVCbKgRQH0O2F5N/
# xLYiJJCfMpqHcRRQEOSDOjOWzO6rU2WBwPSiQTJe9PFSJupnNgvvaHuHFJVr8AJw
# lUomMUwalnX3iM3ox2H//SkURwgUjiYRv2RoBkTzD2qwPwj2ulyfvtBNvnSBOl2U
# 9klA0bvypJInKlJfoxwTlSX0bQ8cyWiJyH/+XJ2aFTqR1Yz3UBiTwLKc+lxoM87G
# bJgGA5vSH72fNIV9pzlwz4fCO1pt81CSIf6ZJUzeSWAdOuM2VVZVNKaA1dCNfRyw
# O3ACp2JT/ZB6GZ1FIF56+0U3OKtdY/qflm0h2QVXtO7R0Gl4wUWFVjYX8ojBPALy
# e0uQUaGCF60wghepBgorBgEEAYI3AwMBMYIXmTCCF5UGCSqGSIb3DQEHAqCCF4Yw
# gheCAgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFaBgsqhkiG9w0BCRABBKCCAUkEggFF
# MIIBQQIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFlAwQCAQUABCAVcT/PJqVIPTLj
# rKoXzPE4E3eRCN3enwlicXTqAcXxygIGatx0o1ilGBMyMDI2MTEwMjE1NDYzOS43
# NzRaMASAAgH0oIHZpIHWMIHTMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGlu
# Z3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBv
# cmF0aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJlbGFuZCBPcGVyYXRpb25zIExp
# bWl0ZWQxJzAlBgNVBAsTHm5TaGllbGQgVFNTIEVTTjo0MDFBLTA1RTAtRDk0NzEl
# MCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZaCCEfswggcoMIIF
# EKADAgECAhMzAAACGV6y2FR19LGNAAEAAAIZMA0GCSqGSIb3DQEBCwUAMHwxCzAJ
# BgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25k
# MR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jv
# c29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMB4XDTI1MDgxNDE4NDgyNloXDTI2MTEx
# MzE4NDgyNlowgdMxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAw
# DgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24x
# LTArBgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5kIE9wZXJhdGlvbnMgTGltaXRlZDEn
# MCUGA1UECxMeblNoaWVsZCBUU1MgRVNOOjQwMUEtMDVFMC1EOTQ3MSUwIwYDVQQD
# ExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNlMIICIjANBgkqhkiG9w0BAQEF
# AAOCAg8AMIICCgKCAgEApqFIyUkzIyxpL3Q03WmLuy4G9YIUScznhKr+cHOT+/u7
# ParxI96gxxb1WrWuAxB8qjGLfsbImx8V3ouK1nUcf+R/nsnXas5/iTgV/Tl3QTRG
# T0DeuXBNbpHqc+wC1NiTyA76gLnirvSBEoBzlrpNQFEnuwdbPLCLpTS3KWSCu5J0
# 2b+RFWR/kcFzVxnhoE3gIaeURtrGKGBZGKLBXvqggkDENtKkvtvRT32xLvAvL/Rp
# Reu5z18ZojCs72ZSoa74Dy8YbaWsDm3OZOpJRZxZsPKCHZ6xNqgFKf0xNHj0t9v0
# Q3W+2z5gAVaasJJCvR52Sl0XJ2AOf3l0LSetXgUA5gD5IQ1RvEslTmNnSouTrGID
# 3D1njY7mBu0puiIdPK2jK/1Weef2+YR4cQpWQkeBZmXidh9AuWdlwxKQL15LJ6K2
# dw8y/t/PBhmLyt6QAf0CepWRdgZnMytVAUuWHwlZRV9JLY7aX8D55eL9+cOLpX3b
# GNOmN24UpIW8qtZaqXaesFvIOW23JNLhaaQVvObr1eu7GE/5Mn43e+/DbtdYl/bL
# P2IQ1xYEJdSbcUkDFfW3KlZEh+nBKDtaRnNRkbgIgxIbKdT38OKQwZ/aA4uSsiAg
# 6nEPiWBHGuytIo5wU75M5VdjhEqqTHfXYu8BJi6GTzvWT+9ekfMXezqCkksxaG8C
# AwEAAaOCAUkwggFFMB0GA1UdDgQWBBSAaOo5HWatNzqZn1IF1fcD6nr3ITAfBgNV
# HSMEGDAWgBSfpxVdAF5iXYP05dJlpxtTNRnpcjBfBgNVHR8EWDBWMFSgUqBQhk5o
# dHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNyb3NvZnQlMjBU
# aW1lLVN0YW1wJTIwUENBJTIwMjAxMCgxKS5jcmwwbAYIKwYBBQUHAQEEYDBeMFwG
# CCsGAQUFBzAChlBodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRz
# L01pY3Jvc29mdCUyMFRpbWUtU3RhbXAlMjBQQ0ElMjAyMDEwKDEpLmNydDAMBgNV
# HRMBAf8EAjAAMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMA4GA1UdDwEB/wQEAwIH
# gDANBgkqhkiG9w0BAQsFAAOCAgEAXxzVZLLXBFfoCCCTiY7MHXdb7civJSTfrHYJ
# C5Ok2NN75NpzTMT9V2TcIQjfQ3AFUbh1NBAYtMUuwxC6D4ceEXG5lXAnbvkC9Yje
# LVDRyImXYYmft7z+Qpl9t3C/8a0tiqnOz8Ue8/DYLtMTgvWMnsqLNjILDaImOfnH
# I36TLCjGFe8RYLXGdCUdOLlfAdMGePxSTA3TAAOc+GQbmPWjrguLWbxvnl3NVjRv
# rBZVkxFMoVZH0f7qGwDOShjpnv5nYnQ48ufL0uBz52RbPGdX4Fv9+UGOrBprmcHz
# mIutFtJec2Y4kujNtTK2wBGgWscEOVhFiaVdje8VLJ7MVNKE5TmsuGM3jTLr1nuR
# 5AFGs3UKkP7g3cQD4cHK7XdLiTm7e606QJ+WqeQsADYE9dvU9wIUbI9Dl4UcIErF
# w+FHaWSTrkfJ4SvLmhKnl5khhpJ1sF3z6e1BxepUliXHqzRLiHWihWIWESF8IHEl
# F3POxbP4VJqHBiYvaXMV0SyRgwoD6zXddbUnX9WR6JL2BlqAjjHxINwelsp/VhxA
# WThzuMA58LxvE/VAzjfFF4Wm7a1ZALmJVw3oL/s/uxo1Op4tcT+hfZ9uN1htC1JN
# 4DuRqFfLttjuoAmUQobO5zUFRzvCn8Ck/hiO+bzR15sqkjlxLMyMjpkc/ef4SUUi
# kD468vUwggdxMIIFWaADAgECAhMzAAAAFcXna54Cm0mZAAAAAAAVMA0GCSqGSIb3
# DQEBCwUAMIGIMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4G
# A1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTIw
# MAYDVQQDEylNaWNyb3NvZnQgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgMjAx
# MDAeFw0yMTA5MzAxODIyMjVaFw0zMDA5MzAxODMyMjVaMHwxCzAJBgNVBAYTAlVT
# MRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQK
# ExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1l
# LVN0YW1wIFBDQSAyMDEwMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA
# 5OGmTOe0ciELeaLL1yR5vQ7VgtP97pwHB9KpbE51yMo1V/YBf2xK4OK9uT4XYDP/
# XE/HZveVU3Fa4n5KWv64NmeFRiMMtY0Tz3cywBAY6GB9alKDRLemjkZrBxTzxXb1
# hlDcwUTIcVxRMTegCjhuje3XD9gmU3w5YQJ6xKr9cmmvHaus9ja+NSZk2pg7uhp7
# M62AW36MEBydUv626GIl3GoPz130/o5Tz9bshVZN7928jaTjkY+yOSxRnOlwaQ3K
# Ni1wjjHINSi947SHJMPgyY9+tVSP3PoFVZhtaDuaRr3tpK56KTesy+uDRedGbsoy
# 1cCGMFxPLOJiss254o2I5JasAUq7vnGpF1tnYN74kpEeHT39IM9zfUGaRnXNxF80
# 3RKJ1v2lIH1+/NmeRd+2ci/bfV+AutuqfjbsNkz2K26oElHovwUDo9Fzpk03dJQc
# NIIP8BDyt0cY7afomXw/TNuvXsLz1dhzPUNOwTM5TI4CvEJoLhDqhFFG4tG9ahha
# YQFzymeiXtcodgLiMxhy16cg8ML6EgrXY28MyTZki1ugpoMhXV8wdJGUlNi5UPkL
# iWHzNgY1GIRH29wb0f2y1BzFa/ZcUlFdEtsluq9QBXpsxREdcu+N+VLEhReTwDwV
# 2xo3xwgVGD94q0W29R6HXtqPnhZyacaue7e3PmriLq0CAwEAAaOCAd0wggHZMBIG
# CSsGAQQBgjcVAQQFAgMBAAEwIwYJKwYBBAGCNxUCBBYEFCqnUv5kxJq+gpE8RjUp
# zxD/LwTuMB0GA1UdDgQWBBSfpxVdAF5iXYP05dJlpxtTNRnpcjBcBgNVHSAEVTBT
# MFEGDCsGAQQBgjdMg30BATBBMD8GCCsGAQUFBwIBFjNodHRwOi8vd3d3Lm1pY3Jv
# c29mdC5jb20vcGtpb3BzL0RvY3MvUmVwb3NpdG9yeS5odG0wEwYDVR0lBAwwCgYI
# KwYBBQUHAwgwGQYJKwYBBAGCNxQCBAweCgBTAHUAYgBDAEEwCwYDVR0PBAQDAgGG
# MA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU1fZWy4/oolxiaNE9lJBb186a
# GMQwVgYDVR0fBE8wTTBLoEmgR4ZFaHR0cDovL2NybC5taWNyb3NvZnQuY29tL3Br
# aS9jcmwvcHJvZHVjdHMvTWljUm9vQ2VyQXV0XzIwMTAtMDYtMjMuY3JsMFoGCCsG
# AQUFBwEBBE4wTDBKBggrBgEFBQcwAoY+aHR0cDovL3d3dy5taWNyb3NvZnQuY29t
# L3BraS9jZXJ0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcnQwDQYJKoZIhvcN
# AQELBQADggIBAJ1VffwqreEsH2cBMSRb4Z5yS/ypb+pcFLY+TkdkeLEGk5c9MTO1
# OdfCcTY/2mRsfNB1OW27DzHkwo/7bNGhlBgi7ulmZzpTTd2YurYeeNg2LpypglYA
# A7AFvonoaeC6Ce5732pvvinLbtg/SHUB2RjebYIM9W0jVOR4U3UkV7ndn/OOPcbz
# aN9l9qRWqveVtihVJ9AkvUCgvxm2EhIRXT0n4ECWOKz3+SmJw7wXsFSFQrP8DJ6L
# GYnn8AtqgcKBGUIZUnWKNsIdw2FzLixre24/LAl4FOmRsqlb30mjdAy87JGA0j3m
# Sj5mO0+7hvoyGtmW9I/2kQH2zsZ0/fZMcm8Qq3UwxTSwethQ/gpY3UA8x1RtnWN0
# SCyxTkctwRQEcb9k+SS+c23Kjgm9swFXSVRk2XPXfx5bRAGOWhmRaw2fpCjcZxko
# JLo4S5pu+yFUa2pFEUep8beuyOiJXk+d0tBMdrVXVAmxaQFEfnyhYWxz/gq77EFm
# PWn9y8FBSX5+k77L+DvktxW/tM4+pTFRhLy/AsGConsXHRWJjXD+57XQKBqJC482
# 2rpM+Zv/Cuk0+CQ1ZyvgDbjmjJnW4SLq8CdCPSWU5nR0W2rRnj7tfqAxM328y+l7
# vzhwRNGQ8cirOoo6CGJ/2XBjU02N7oJtpQUQwXEGahC0HVUzWLOhcGbyoYIDVjCC
# Aj4CAQEwggEBoYHZpIHWMIHTMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGlu
# Z3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBv
# cmF0aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJlbGFuZCBPcGVyYXRpb25zIExp
# bWl0ZWQxJzAlBgNVBAsTHm5TaGllbGQgVFNTIEVTTjo0MDFBLTA1RTAtRDk0NzEl
# MCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZaIjCgEBMAcGBSsO
# AwIaAxUAMXYp/Wqqdyb0enigrLfxl0InAz6ggYMwgYCkfjB8MQswCQYDVQQGEwJV
# UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE
# ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGlt
# ZS1TdGFtcCBQQ0EgMjAxMDANBgkqhkiG9w0BAQsFAAIFAO6Sz6kwIhgPMjAyNjEx
# MDIwOTAwMjVaGA8yMDI2MTEwMzA5MDAyNVowdDA6BgorBgEEAYRZCgQBMSwwKjAK
# AgUA7pLPqQIBADAHAgEAAgIRiDAHAgEAAgITNTAKAgUA7pQhKQIBADA2BgorBgEE
# AYRZCgQCMSgwJjAMBgorBgEEAYRZCgMCoAowCAIBAAIDB6EgoQowCAIBAAIDAYag
# MA0GCSqGSIb3DQEBCwUAA4IBAQC0u1Lkq4X1FfzrLEWsQnZEhdAOw0blVlO2kocq
# 4/V4RYlvzvwePrx/7QNtDGvHxwlo0WcS98OM8HerO2Be0Gf2W/O956CRdgsLMueC
# 3lcpkh3PrA5C1W8Gk0914isEDXJoBZksB53sPC8IfkvmpqHp8LthDcDJO2GbI+JH
# jFkwXZTHvFuPoLVR7zaKOxV/SykoPQ9UMdTmMp9DDfq8vBwhRTqRKemJ5qReT9Zr
# 8I8xjoGwXBL6S5iiCds1EMdiR/bw+KEVpqujd20sQUjB33pNU7n3A/H1JvGrhh7A
# j7Ot5JnRFsfpNBKeFxclJPx5XgD/VrKoqoQYBTBQBrhvmsOSMYIEDTCCBAkCAQEw
# gZMwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcT
# B1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UE
# AxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTACEzMAAAIZXrLYVHX0sY0A
# AQAAAhkwDQYJYIZIAWUDBAIBBQCgggFKMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0B
# CRABBDAvBgkqhkiG9w0BCQQxIgQg6mddlWlFw8CFObiDyEfkc4LZdew54sABsVse
# OwIa8RowgfoGCyqGSIb3DQEJEAIvMYHqMIHnMIHkMIG9BCDckX633E1y1EF32V18
# zQcrsgjzI9+3Le7mlvk2OebthjCBmDCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1w
# IFBDQSAyMDEwAhMzAAACGV6y2FR19LGNAAEAAAIZMCIEIOZAy2CjR0CR+bOJ8osK
# ptYjTlynL0/WfxRWQRDYIfcAMA0GCSqGSIb3DQEBCwUABIICAENEC6dajRH2580E
# /u15GQnEIbX4lXcLgOG3gESwJk3BDdKt/sq5tpffisDboXb9DnqlUTDnd+xSKF0v
# P8nQrDurb8I7HEl1XQG7yu7dv1MB1qreKGgYHThLQkWlRItkvg3dSRsxu7MQR4KG
# k8Lj3qlwGY0gXVK+PAkhRPgfAz+lhiBd3CZmJxvA4hUQERFS0AtTIE/xhvqqI1XF
# B3RNDixML6Vs5tCtD/mrfqHXAjnpzJfPzxdFLjEqXl1YWW80p1/GI8hYpAhxv8bo
# q1lihrA+RdXScQymrAdSP+tDIdk69tNxPUto+7HNVznLhXLbvYmJi+ETQB2MH6HL
# 8bn0NDeHn0DhIj03fhVdfkwoDRJog43bvBtde42o7SBnLFKlVcrCvDFVDXgtlBxB
# 5kgJUJLZ2GpNL9x2KYJOB3quSjUJMOrNaOqB1nn3m/MrMKE9XjE4bp1g2mor2X2c
# FgUAkS84pJLcAZ8v+anqksUkMkCRgvrenhzGvIY97paLN7hD+RujYuweagW0Z3Vj
# vphEloYXrIlDuSXEVWyUzcaPBhEBdNiN8hs13o3sA6RGCVuvWrqeRj9PT4k60uz3
# oMyt2mQIJ2iEWjycaKpsv7VGghsjjSPhKvH03PVZj7iJ8y2muqu5ChbrMorFaiLK
# VW1E677vqP7DbOdYQ3QR/ab9dYR6
# SIG # End signature block