Engines/Terraform/Invoke-AvmTerraformTransform.ps1

function Resolve-AvmMapotfConfigDir {
    <#
    .SYNOPSIS
        Resolve a scoped directory holding vendored mapotf configs.

    .DESCRIPTION
        Returns the absolute path to the '*.mptf.hcl' bundle passed to
        'mapotf transform --mptf-dir'. Resolution order:

          1. $env:AVM_MPTF_CONFIG_DIR/<profile>.
          2. <Root>/config/mapotf/<profile>.
          3. <ModuleRoot>/Resources/mapotf/<profile>.

        Each candidate must be a directory containing at least one
        '*.mptf.hcl' file. Throws AvmConfigurationException when none
        resolve, so the transform engine surfaces as 'skipped' (a deliberate
        placeholder) rather than running mapotf against an empty config set.

    .PARAMETER Root
        The consumer repository root, used to locate an optional
        'config/mapotf/<profile>' override. Pass $Context.Root.

    .PARAMETER Profile
        Config profile to resolve: common, module, root, module-call, variable-order, example,
        provider-cleanup, location-inspect, unit-test-inspect, unit-test, or an optional test override.

    .PARAMETER Optional
        Return $null instead of throwing when the profile does not exist.

    .OUTPUTS
        [string] absolute path to the resolved config directory.
    #>

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

        [Parameter(Mandatory)]
        [ValidateSet('common', 'module', 'root', 'module-call', 'variable-order', 'example', 'provider-cleanup', 'test', 'location-inspect', 'unit-test-inspect', 'unit-test')]
        [string] $ProfileName,

        [switch] $Optional
    )

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

    $candidates = New-Object System.Collections.Generic.List[string]

    if ($env:AVM_MPTF_CONFIG_DIR) {
        $candidates.Add((Join-Path $env:AVM_MPTF_CONFIG_DIR $ProfileName))
    }

    $candidates.Add((Join-Path $Root (Join-Path 'config' (Join-Path 'mapotf' $ProfileName))))

    $moduleRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
    $candidates.Add((Join-Path $moduleRoot (Join-Path 'Resources' (Join-Path 'mapotf' $ProfileName))))

    foreach ($candidate in $candidates) {
        if (-not $candidate) { continue }
        if (-not (Test-Path -LiteralPath $candidate -PathType Container)) { continue }
        $configs = @(Get-ChildItem -LiteralPath $candidate -Filter '*.mptf.hcl' -File -ErrorAction SilentlyContinue)
        if ($configs.Count -gt 0) {
            return (Resolve-Path -LiteralPath $candidate).ProviderPath
        }
    }

    if ($Optional) {
        return $null
    }

    throw [AvmConfigurationException]::new(
        ("Cannot resolve the mapotf '{0}' config profile (looked in: {1}). " -f $ProfileName, ($candidates -join '; ')) +
        ("The profile normally ships inside the module under Resources/mapotf/{0}; " -f $ProfileName) +
        ("set AVM_MPTF_CONFIG_DIR or add config/mapotf/{0}/*.mptf.hcl to override it." -f $ProfileName))
}

function Get-AvmTerraformFile {
    <#
    .SYNOPSIS
        Enumerate the '*.tf' files mapotf would touch under a module root.

    .DESCRIPTION
        Returns FileInfo records for every '*.tf' and '*.tftest.hcl' file beneath $Root,
        excluding any path segment that begins with '.' (e.g. '.terraform',
        '.git') or equals 'node_modules'. Used by Invoke-AvmTerraformTransform
        to snapshot file hashes before/after the transform so the engine can
        report which files mapotf changed. Always returns an array (empty
        when nothing matches) so callers can rely on '.Count'.

    .PARAMETER Root
        The module root to walk.

    .OUTPUTS
        [object[]] of System.IO.FileInfo.
    #>

    [CmdletBinding()]
    [OutputType([object[]])]
    param(
        [Parameter(Mandatory)]
        [string] $Root
    )

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

    $terraformFiles = @(Get-ChildItem -LiteralPath $Root -Recurse -File -Filter '*.tf' -ErrorAction SilentlyContinue)
    $testFiles = @(Get-ChildItem -LiteralPath $Root -Recurse -File -Filter '*.tftest.hcl' -ErrorAction SilentlyContinue)
    return @(
        @($terraformFiles + $testFiles) |
            Where-Object {
                $rel = [System.IO.Path]::GetRelativePath($Root, $_.FullName)
                $parts = $rel -split '[\\/]'
                -not ($parts | Where-Object { $_.StartsWith('.') -or $_ -eq 'node_modules' })
            }
    )
}

function Get-AvmTerraformTransformTarget {
    [CmdletBinding()]
    [OutputType([pscustomobject[]])]
    param(
        [Parameter(Mandatory)]
        [string] $Root
    )

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

    $targets = New-Object System.Collections.Generic.List[object]
    $scopes = @(Get-AvmMetadataScope -Context ([pscustomobject]@{
                Root      = $Root
                Ecosystem = 'terraform'
            }))
    foreach ($scope in $scopes) {
        if ($scope.ChildModule -and
            @(Get-ChildItem -LiteralPath $scope.Path -File -Filter '*.tf').Count -eq 0) {
            continue
        }

        $metadataPath = Join-Path $scope.Path 'metadata.json'
        $metadataFile = @(Get-ChildItem -LiteralPath $scope.Path -File |
                Where-Object { $_.Name -ceq 'metadata.json' })
        if ($metadataFile.Count -eq 0) {
            throw [AvmConfigurationException]::new(
                "Terraform module '$($scope.Path)' requires metadata.json before telemetry transformation.")
        }
        try {
            $metadata = Get-Content -LiteralPath $metadataPath -Raw -ErrorAction Stop |
                ConvertFrom-Json -AsHashtable -ErrorAction Stop
        }
        catch {
            throw [AvmConfigurationException]::new(
                "Cannot read Terraform module metadata '$metadataPath': $($_.Exception.Message)")
        }
        if ($metadata -isnot [System.Collections.IDictionary]) {
            throw [AvmConfigurationException]::new(
                "Terraform module metadata '$metadataPath' must contain a JSON object.")
        }

        $prefix = [string]$metadata['telemetryIdPrefix']
        if ($metadata.Contains('telemetryIdPrefix') -and [string]::IsNullOrWhiteSpace($prefix)) {
            throw [AvmConfigurationException]::new(
                "Terraform module metadata '$metadataPath' has an empty telemetryIdPrefix.")
        }
        if ($prefix -and $prefix -cnotmatch '^46d3xtrf\.(res|ptn|utl)\.[0-9a-f]{7}$') {
            throw [AvmConfigurationException]::new(
                "Terraform module metadata '$metadataPath' telemetryIdPrefix must end in seven lowercase hexadecimal characters.")
        }
        $profiles = if ($prefix) {
            @('root', 'module', 'common')
        }
        else {
            @('module', 'common')
        }
        $targets.Add([pscustomobject]@{
                Path     = $scope.Path
                Scope    = if ($scope.ChildModule) { 'module' } else { 'root' }
                Profiles = $profiles
            })
    }

    $examplesDir = Join-Path $Root 'examples'
    if (Test-Path -LiteralPath $examplesDir -PathType Container) {
        foreach ($example in Get-ChildItem -LiteralPath $examplesDir -Directory -ErrorAction SilentlyContinue | Sort-Object Name) {
            $terraformFiles = @(Get-ChildItem -LiteralPath $example.FullName -File -Filter '*.tf' -ErrorAction SilentlyContinue)
            if ($terraformFiles.Count -eq 0) {
                continue
            }
            $targets.Add([pscustomobject]@{
                    Path     = $example.FullName
                    Scope    = 'example'
                    Profiles = @('example', 'provider-cleanup', 'common')
                })
        }
    }

    foreach ($moduleTarget in @($targets | Where-Object { $_.Profiles -contains 'root' })) {
        $testsDir = Join-Path $moduleTarget.Path 'tests'
        if (Test-Path -LiteralPath $testsDir -PathType Container) {
            $testRoots = Get-ChildItem -LiteralPath $testsDir -Recurse -File -Filter '*.tf' -ErrorAction SilentlyContinue |
                ForEach-Object { $_.Directory.FullName } |
                Sort-Object -Unique
            foreach ($testRoot in $testRoots) {
                $rel = [System.IO.Path]::GetRelativePath($testsDir, $testRoot)
                if (@($rel -split '[\\/]' | Where-Object { $_.StartsWith('.') -or $_ -eq 'node_modules' }).Count -gt 0) {
                    continue
                }
                $targets.Add([pscustomobject]@{
                        Path     = $testRoot
                        Scope    = 'test'
                        Profiles = @('module-call', 'variable-order', 'provider-cleanup', 'test')
                    })
            }
        }
    }

    return $targets.ToArray()
}

function Test-AvmTerraformProviderInUse {
    [CmdletBinding()]
    [OutputType([bool])]
    param(
        [Parameter(Mandatory)]
        [string] $ModulePath,

        [Parameter(Mandatory)]
        [object[]] $ModuleTargets,

        [Parameter(Mandatory)]
        [string] $TestDirectory,

        [Parameter(Mandatory)]
        [ValidateSet('modtm', 'random')]
        [string] $Provider
    )

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

    $modulePrefix = $ModulePath + [System.IO.Path]::DirectorySeparatorChar
    $directories = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal)
    foreach ($target in $ModuleTargets) {
        if ($target.Profiles -contains 'example' -and $target.Path -cne $ModulePath) {
            continue
        }
        if ($target.Path -cne $ModulePath -and
            -not $target.Path.StartsWith($modulePrefix, [System.StringComparison]::Ordinal)) {
            continue
        }
        $null = $directories.Add($target.Path)
    }
    $null = $directories.Add($TestDirectory)
    foreach ($directory in Get-ChildItem -LiteralPath $TestDirectory -Recurse -Directory) {
        $relativePath = [System.IO.Path]::GetRelativePath($TestDirectory, $directory.FullName)
        if (@($relativePath -split '[\\/]' | Where-Object { $_.StartsWith('.') -or $_ -eq 'node_modules' }).Count -eq 0) {
            $null = $directories.Add($directory.FullName)
        }
    }

    foreach ($directory in $directories) {
        foreach ($file in Get-ChildItem -LiteralPath $directory -File) {
            if ($file.Name.EndsWith('.tf.json', [System.StringComparison]::OrdinalIgnoreCase)) {
                try {
                    Assert-AvmTerraformLocalsOnlyJson -Path $file.FullName `
                        -ReviewMessage "Preserving $Provider mock: JSON configuration needs dependency review"
                }
                catch [AvmConfigurationException] {
                    Write-AvmLog $_.Exception.Message -Level Verbose | Out-Null
                    return $true
                }
                continue
            }
            if (-not $file.Name.EndsWith('.tf', [System.StringComparison]::OrdinalIgnoreCase)) {
                continue
            }
            $source = [System.IO.File]::ReadAllText($file.FullName)
            if ($source -match ('(?m)^[ \t]*' + $Provider + '[ \t]*=[ \t]*\{') -or
                $source -match ('(?:hashicorp|azure)/' + $Provider) -or
                $source -match ('(?m)^[ \t]*(?:resource|data|ephemeral)[ \t]+"' + $Provider + '_[^"]+"[ \t]+"[^"]+"')) {
                return $true
            }
            $moduleBlocks = [regex]::Matches($source, '(?m)^[ \t]*module[ \t]+"[^"]+"[ \t]*\{')
            if ([regex]::Matches($source, '(?m)^[ \t]*module\b(?!\.)').Count -ne $moduleBlocks.Count) {
                Write-AvmLog "Preserving $Provider mock: module declarations in '$($file.FullName)' need dependency review." -Level Verbose | Out-Null
                return $true
            }
            foreach ($block in $moduleBlocks) {
                $sourceMatch = [regex]::Match(
                    $source.Substring($block.Index + $block.Length),
                    '\A\s*source[ \t]*=[ \t]*"(?<path>\.\.?/[^"]+)"')
                if (-not $sourceMatch.Success) {
                    Write-AvmLog "Preserving $Provider mock: a module source in '$($file.FullName)' is not a known local dependency." -Level Verbose | Out-Null
                    return $true
                }
                $dependencyPath = [System.IO.Path]::GetFullPath($sourceMatch.Groups['path'].Value, $directory)
                if (-not $directories.Contains([System.IO.Path]::TrimEndingDirectorySeparator($dependencyPath))) {
                    Write-AvmLog "Preserving $Provider mock: module dependency '$dependencyPath' was not scanned." -Level Verbose | Out-Null
                    return $true
                }
            }
        }
    }
    return $false
}

function Test-AvmTerraformScopedProviderInUse {
    [CmdletBinding()]
    [OutputType([bool])]
    param(
        [Parameter(Mandatory)] [object] $Scope,
        [Parameter(Mandatory)] [object[]] $ModuleTargets,
        [AllowEmptyCollection()] [string[]] $TargetPaths = @(),
        [Parameter(Mandatory)] [hashtable] $Cache,
        [Parameter(Mandatory)] [ValidateSet('modtm', 'random')] [string] $Provider
    )

    Set-StrictMode -Version 3.0
    $ErrorActionPreference = 'Stop'
    $testDirectory = Split-Path -Parent $Scope.File.FullName
    $ownerPrefix = $Scope.Owner.Path + [System.IO.Path]::DirectorySeparatorChar
    $ownerScannedPaths = @($ModuleTargets | Where-Object {
            $_.Profiles -notcontains 'example' -and
            $_.Path.StartsWith($ownerPrefix, [System.StringComparison]::Ordinal)
        } | ForEach-Object { $_.Path })
    foreach ($path in @($Scope.Owner.Path) + $TargetPaths) {
        if ($path -cin $ownerScannedPaths) {
            continue
        }
        $key = "$Provider|$path|$testDirectory"
        if (-not $Cache.ContainsKey($key)) {
            $Cache[$key] = Test-AvmTerraformProviderInUse `
                -ModulePath $path -ModuleTargets $ModuleTargets -TestDirectory $testDirectory -Provider $Provider
        }
        if ($Cache[$key]) {
            return $true
        }
    }
    return $false
}

function Test-AvmTerraformScopedRandomProviderInUse {
    [CmdletBinding()]
    [OutputType([bool])]
    param(
        [Parameter(Mandatory)] [object] $Scope,
        [Parameter(Mandatory)] [object[]] $ModuleTargets,
        [AllowEmptyCollection()] [string[]] $TargetPaths = @(),
        [Parameter(Mandatory)] [hashtable] $Cache
    )

    Test-AvmTerraformScopedProviderInUse -Scope $Scope -ModuleTargets $ModuleTargets `
        -TargetPaths $TargetPaths -Cache $Cache -Provider random
}

function Remove-AvmLegacyTelemetryTestMock {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)]
        [string] $Root,

        [Parameter(Mandatory)]
        [object[]] $ModuleTargets,

        [object[]] $UnitTestPlans = @()
    )

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

    $changes = [System.Collections.Generic.List[object]]::new()
    $emptyMock = '(?m)^[ \t]*mock_provider[ \t]+"modtm"[ \t]*\{[ \t\r\n]*\}[ \t]*(?:\r?\n)?'
    $customMock = '(?m)^[ \t]*mock_provider[ \t]+"modtm"[ \t]*\{'
    $azapiMock = '(?m)^[ \t]*mock_provider[ \t]+"azapi"[ \t]*\{'
    $emptyAzapiMock = '(?m)^[ \t]*mock_provider[ \t]+"azapi"[ \t]*\{[ \t\r\n]*\}[ \t]*(?:\r?\n)?'
    $legacyReference = '(?<![A-Za-z0-9_])modtm_telemetry\.telemetry(?![A-Za-z0-9_])'
    $legacyIdentifierReference = '(?<![A-Za-z0-9_-])random_uuid(?:\s|/\*[\s\S]*?\*/|//[^\r\n]*|#[^\r\n]*)*\.' +
    '(?:\s|/\*[\s\S]*?\*/|//[^\r\n]*|#[^\r\n]*)*telemetry(?![A-Za-z0-9_-])'
    $emptyRandomMock = '(?m)^[ \t]*mock_provider[ \t]+"random"[ \t]*\{[ \t\r\n]*\}[ \t]*(?:\r?\n)?'
    $randomMock = '(?m)^[ \t]*mock_provider[ \t]+"random"[ \t]*\{'
    $randomUse = @{}
    $modtmUse = @{}

    foreach ($scope in Get-AvmTerraformTestFileScope -Root $Root -ModuleTargets $ModuleTargets) {
        $file = $scope.File
        $owner = $scope.Owner
        $unitPlan = $UnitTestPlans | Where-Object { $_.Path -ceq $file.FullName } | Select-Object -First 1
        $validatedMocks = $null -ne $unitPlan -and
        $null -ne $unitPlan.PSObject.Properties['MockMigrationValidated'] -and
        $unitPlan.MockMigrationValidated -eq $true
        $retiredProviderReferences = if ($validatedMocks) { $unitPlan.RetiredProviderReferences } else { @{ modtm = 0; random = 0 } }
        $instrumentedTargets = @($ModuleTargets |
                Where-Object { $null -ne $unitPlan -and $_.Path -cin $unitPlan.TargetPaths -and $_.Profiles -contains 'root' })
        if ($owner.Profiles -notcontains 'root' -and $instrumentedTargets.Count -eq 0) {
            continue
        }

        $original = [System.IO.File]::ReadAllText($file.FullName)
        $identifierReferences = [regex]::Matches($original, $legacyIdentifierReference,
            [System.Text.RegularExpressions.RegexOptions]::CultureInvariant, [TimeSpan]::FromSeconds(1)).Count
        $validatedIdentifierReferences = if ($validatedMocks -and
            $null -ne $unitPlan.PSObject.Properties['TelemetryCountMigration']) {
            $unitPlan.TelemetryCountMigration.ReferenceCount
        }
        else { 0 }
        if ($identifierReferences -ne $validatedIdentifierReferences) {
            throw [AvmConfigurationException]::new(
                "Test file '$($file.FullName)' still references retired random_uuid.telemetry; only complete zero-or-one count assertions, optionally prefixed by a ModTM or AzAPI telemetry-count assertion joined with &&, on instrumented unit-test targets can be migrated automatically. Review other references manually.")
        }
        $isUnitTest = $scope.IsUnitTest
        $retiredMock = [regex]::Match($original, $emptyMock)
        $withoutModtmMocks = [regex]::Replace($original, $emptyMock, '')
        if ([regex]::IsMatch($withoutModtmMocks, $customMock) -and
            -not ($validatedMocks -and $null -ne $unitPlan.PSObject.Properties['RetainModtmMock'] -and $unitPlan.RetainModtmMock)) {
            throw [AvmConfigurationException]::new(
                "Test file '$($file.FullName)' contains a non-empty modtm mock; remove or rewrite it manually before telemetry migration.")
        }
        $retainModtm = if ($null -ne $unitPlan -and $null -ne $unitPlan.PSObject.Properties['RetainModtmMock']) {
            $unitPlan.RetainModtmMock
        }
        else {
            $targetPaths = @()
            if ($null -ne $unitPlan) { $targetPaths = @($unitPlan.TargetPaths) }
            Test-AvmTerraformScopedProviderInUse -Scope $scope -ModuleTargets $ModuleTargets `
                -TargetPaths $targetPaths -Cache $modtmUse -Provider modtm
        }
        $updated = if ($retainModtm) { $original } else { $withoutModtmMocks }
        $retainedModtmReferences = if ($validatedMocks -and
            $null -ne $unitPlan.PSObject.Properties['RetainedProviderReferences']) {
            $unitPlan.RetainedProviderReferences.modtm
        }
        else { 0 }
        $emptyAzapi = [regex]::Match($updated, $emptyAzapiMock)
        $hasAzapiMock = [regex]::IsMatch($updated, $azapiMock)
        $needsAzapiMock = ($retiredMock.Success -and -not $hasAzapiMock) -or
        ($validatedMocks -and $unitPlan.AddDefaultAzapiMock)
        if ($isUnitTest -and ($retiredMock.Success -or $emptyAzapi.Success -or $validatedMocks)) {
            $modtmReferences = Get-AvmTerraformRetiredTestProviderReferenceCount -Text $updated -Provider modtm
            if ($updated -match '(?m)^[ \t]*provider[ \t]+"azapi"[ \t]*\{' -or
                ($null -eq $unitPlan -and $updated -match '(?m)^[ \t]*module[ \t]*\{') -or
                $modtmReferences -ne ($retiredProviderReferences.modtm + $retainedModtmReferences) -or
                (-not $validatedMocks -and $hasAzapiMock -and $updated -match '\balias[ \t]*=')) {
                throw [AvmConfigurationException]::new(
                    "Cannot automatically migrate telemetry mocks in '$($file.FullName)': an authored provider, alias, provider mapping, or delegated test module needs review.")
            }
        }
        if ($isUnitTest -and ($needsAzapiMock -or $emptyAzapi.Success)) {
            $lineEnding = if ($original.Contains("`r`n")) { "`r`n" } else { "`n" }
            $replacementMock = @'
mock_provider "azapi" {
  mock_data "azapi_client_config" {
    defaults = {
      subscription_resource_id = "/subscriptions/00000000-0000-0000-0000-000000000000"
    }
  }
}
'@

            $replacementMock = ($replacementMock.Replace("`r`n", "`n").TrimEnd() + "`n").Replace("`n", $lineEnding)
            if ($emptyAzapi.Success) {
                $updated = $updated.Remove($emptyAzapi.Index, $emptyAzapi.Length).Insert($emptyAzapi.Index, $replacementMock)
            }
            else {
                $index = if ($retiredMock.Success) { $retiredMock.Index } else { 0 }
                $updated = $updated.Insert($index, $replacementMock)
            }
        }
        $updated = [regex]::Replace($updated, $legacyReference, 'azapi_resource.telemetry')
        if ($isUnitTest -and [regex]::IsMatch($updated, $randomMock)) {
            $targetPaths = @()
            if ($null -ne $unitPlan) {
                $targetPaths = @($unitPlan.TargetPaths)
            }
            $randomInUse = Test-AvmTerraformScopedRandomProviderInUse -Scope $scope `
                -ModuleTargets $ModuleTargets -TargetPaths $targetPaths -Cache $randomUse
            if (-not $randomInUse) {
                $withoutEmptyMocks = [regex]::Replace($updated, $emptyRandomMock, '')
                $randomReferences = Get-AvmTerraformRetiredTestProviderReferenceCount -Text $withoutEmptyMocks -Provider random
                if ([regex]::IsMatch($withoutEmptyMocks, $randomMock) -or
                    $randomReferences -ne $retiredProviderReferences.random -or
                    ($null -eq $unitPlan -and $withoutEmptyMocks -match '(?m)^[ \t]*module[ \t]*\{')) {
                    throw [AvmConfigurationException]::new(
                        "Test file '$($file.FullName)' still uses a random mock or resource after the telemetry-only random provider was removed; review it manually.")
                }
                $updated = $withoutEmptyMocks
            }
        }
        if ($updated -cne $original) {
            $changes.Add([pscustomobject]@{ Path = $file.FullName; Content = $updated })
        }
    }

    $encoding = [System.Text.UTF8Encoding]::new($false)
    foreach ($change in $changes) {
        if ($PSCmdlet.ShouldProcess($change.Path, 'migrate retired telemetry test mocks and references')) {
            [System.IO.File]::WriteAllText($change.Path, $change.Content, $encoding)
        }
    }
}

function Get-AvmTerraformLocationInspection {
    [CmdletBinding()]
    [OutputType([hashtable])]
    param(
        [Parameter(Mandatory)][string] $Path,
        [Parameter(Mandatory)][object] $Options
    )

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

    $result = Invoke-AvmProcess -FilePath $Options.ToolPath -ArgumentList @(
        'debug', '--tf-dir', $Path,
        '--mptf-dir', $Options.ProfileDirs['location-inspect'],
        '--eval', 'data.variable.location.result'
    ) -WorkingDirectory $Path -EnvVars $Options.EnvVars
    try {
        $inspection = ConvertFrom-Json -InputObject $result.StdOut -AsHashtable -ErrorAction Stop
    }
    catch {
        throw [AvmConfigurationException]::new(
            "Cannot inspect the location input in '$Path': invalid MaPoTF JSON. $($_.Exception.Message)")
    }
    if ($inspection -isnot [System.Collections.IDictionary] -or
        $inspection.Count -gt 1 -or ($inspection.Count -eq 1 -and -not $inspection.Contains('location'))) {
        throw [AvmConfigurationException]::new("Cannot inspect the location input in '$Path': invalid variable inspection.")
    }
    if ($inspection.Count -eq 0) {
        return $null
    }
    $declaration = $inspection['location']
    if ($declaration -isnot [System.Collections.IDictionary] -or
        $declaration['mptf'] -isnot [System.Collections.IDictionary] -or
        $declaration.mptf['block_address'] -cne 'variable.location' -or
        $declaration.mptf['range'] -isnot [System.Collections.IDictionary]) {
        throw [AvmConfigurationException]::new("Cannot inspect the location input in '$Path': missing native source metadata.")
    }
    return $declaration
}

function Set-AvmRequiredLocationLintDirective {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)][object[]] $Targets,
        [Parameter(Mandatory)][object] $Options
    )

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

    $directive = '# tflint-ignore: terraform_unused_declarations'
    foreach ($target in $Targets) {
        $declaration = Get-AvmTerraformLocationInspection -Path $target.Path -Options $Options
        if ($null -eq $declaration -or $declaration.Count -ne 4 -or
            $declaration['type'] -cne 'string' -or $declaration['nullable'] -isnot [bool] -or
            $declaration.nullable -or
            $declaration['description'] -cne "The Azure region for this module's resources or telemetry deployment." -or
            $declaration.mptf.range['file_name'] -cne 'variables.tf') {
            continue
        }

        $path = Join-Path $target.Path 'variables.tf'
        $content = [System.IO.File]::ReadAllText($path)
        $lines = @($content -split '\r?\n')
        $startLine = $declaration.mptf.range['start_line']
        if (($startLine -isnot [int] -and $startLine -isnot [long]) -or
            $startLine -lt 1 -or $startLine -gt $lines.Count -or
            $lines[$startLine - 1] -cne 'variable "location" {') {
            throw [AvmConfigurationException]::new("Cannot annotate the generated location input in '$path': invalid native source range.")
        }
        if ($startLine -gt 1 -and $lines[$startLine - 2] -ceq $directive) {
            continue
        }
        $offset = [regex]::Matches($content, '(?m)^')[$startLine - 1].Index
        $lineEnding = if ($content.Contains("`r`n")) { "`r`n" } else { "`n" }
        if ($PSCmdlet.ShouldProcess($path, 'exempt the newly generated mandatory location input from the unused-declaration rule')) {
            [System.IO.File]::WriteAllText(
                $path, $content.Insert($offset, $directive + $lineEnding), [System.Text.UTF8Encoding]::new($false))
        }
    }
}

function Set-AvmTelemetryTagLintDirective {
    [CmdletBinding(SupportsShouldProcess)]
    param([Parameter(Mandatory)][object[]] $Targets)

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

    $directive = '# tflint-ignore: avm_azapi_resource_tags_required'
    $encoding = [System.Text.UTF8Encoding]::new($false)
    foreach ($target in $Targets | Where-Object { $_.Profiles -contains 'root' }) {
        $path = Join-Path $target.Path 'main.telemetry.tf'
        if (-not (Test-Path -LiteralPath $path -PathType Leaf)) {
            throw [AvmConfigurationException]::new("Instrumented Terraform module '$($target.Path)' has no main.telemetry.tf.")
        }

        $original = [System.IO.File]::ReadAllText($path)
        $content = $original
        $resources = [regex]::Matches(
            $content,
            '(?ms)^resource[ \t]+"azapi_resource"[ \t]+"telemetry"[ \t]*\{(?<body>.*?)^\}')
        if ($resources.Count -ne 1) {
            throw [AvmConfigurationException]::new("Expected one azapi_resource.telemetry block in '$path'.")
        }

        $body = $resources[0].Groups['body']
        $cleanBody = [regex]::Replace(
            $body.Value,
            '(?m)^[ \t]*# tflint-ignore: avm_azapi_resource_tags_required\r?\n',
            '')
        if ($cleanBody -cne $body.Value) {
            $content = $content.Remove($body.Index, $body.Length).Insert($body.Index, $cleanBody)
            $resources = [regex]::Matches(
                $content,
                '(?ms)^resource[ \t]+"azapi_resource"[ \t]+"telemetry"[ \t]*\{(?<body>.*?)^\}')
        }

        $offset = $resources[0].Index
        $priorLines = @($content.Substring(0, $offset) -split '\r?\n')
        if ($priorLines.Count -lt 2 -or $priorLines[-2] -cne $directive) {
            $lineEnding = if ($content.Contains("`r`n")) { "`r`n" } else { "`n" }
            $content = $content.Insert($offset, $directive + $lineEnding)
        }

        if ($content -cne $original -and
            $PSCmdlet.ShouldProcess($path, 'exempt generated telemetry deployment from the standard resource-tag rule')) {
            [System.IO.File]::WriteAllText($path, $content, $encoding)
        }
    }
}

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

        [Parameter(Mandatory)]
        [object[]] $Targets
    )

    Set-StrictMode -Version 3.0
    $issues = [System.Collections.Generic.List[object]]::new()
    foreach ($target in $Targets | Where-Object { $_.Profiles -contains 'root' -or $_.Scope -in @('test', 'example') }) {
        foreach ($file in Get-ChildItem -LiteralPath $target.Path -Filter '*.tf' -File) {
            $lineNumber = 0
            foreach ($line in [System.IO.File]::ReadAllLines($file.FullName)) {
                $lineNumber++
                if ($line -cmatch '^\s*(resource|data)\s+"modtm_[^"]+"\s+"[^"]+"') {
                    $issues.Add([pscustomobject][ordered]@{
                            File     = [System.IO.Path]::GetRelativePath($Root, $file.FullName)
                            Line     = $lineNumber
                            Column   = 1
                            Severity = 'error'
                            Code     = 'avm.tf.modtm-remains'
                            Message  = 'An author-owned modtm block remains after telemetry migration; migrate it before removing the modtm provider.'
                        })
                }
            }
        }
    }
    return $issues.ToArray()
}

function Invoke-AvmMapotfTransformTarget {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [object] $Target,

        [Parameter(Mandatory)]
        [object] $Options
    )

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

    $transformArguments = New-Object System.Collections.Generic.List[string]
    $transformArguments.Add('transform')
    foreach ($profileName in $Target.Profiles) {
        $profileDir = $Options.ProfileDirs[$profileName]
        if (-not $profileDir) {
            continue
        }
        $transformArguments.Add('--mptf-dir')
        $transformArguments.Add($profileDir)
    }
    $transformArguments.Add('--tf-dir')
    $transformArguments.Add($Target.Path)
    if ($Target.Scope -eq 'test' -and $Target.Profiles -contains 'module-call') {
        $transformArguments.Add('--mptf-var')
        $transformArguments.Add('test_wrapper=true')
    }

    $stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
    $transform = Invoke-AvmProcess `
        -FilePath $Options.ToolPath `
        -ArgumentList $transformArguments.ToArray() `
        -WorkingDirectory $Target.Path `
        -EnvVars $Options.EnvVars `
        -IgnoreExitCode `
        -RetryNetworkFailure `
        -Label ('mapotf transform {0} target' -f $Target.Scope)
    if ($transform.ExitCode -ne 0) {
        $message = Add-AvmProcessFailureDetail `
            -Message ('mapotf transform exited with code {0} for {1} target {2}.' -f $transform.ExitCode, $Target.Scope, $Target.Path) `
            -StdOut $transform.StdOut `
            -StdErr $transform.StdErr
        throw [AvmProcessException]::new($message)
    }
    $stopwatch.Stop()

    Write-AvmLog (
        'transform: {0} target completed in {1}: {2}' -f
        $Target.Scope, (Format-AvmDuration -Duration $stopwatch.Elapsed), $Target.Path
    ) -Level Verbose | Out-Null
}

function Invoke-AvmTerraformTransform {
    <#
    .SYNOPSIS
        Apply the AVM mapotf HCL transforms to a Terraform module.

    .DESCRIPTION
        Engine implementation called by Invoke-AvmTransform when the module
        context is Ecosystem='terraform'. Resolves the 'mapotf' binary via
        Resolve-AvmTool and the vendored config bundle via
        Resolve-AvmMapotfConfigDir, then runs, against $Context.Root:

            mapotf transform --mptf-dir <profile> [...] --tf-dir <target>
            mapotf clean-backup --tf-dir <root>

        Metadata-backed roots and child modules with a telemetryIdPrefix run
        root, module, common. Children without a prefix run module, common.
        Module calls then run module-call, variable-order, common from deepest to root after
        every child has its inputs, forwarding location and enable_telemetry
        where supported. That pass sorts variables after missing location
        inputs are generated, keeping the first transform idempotent.
        Native test-file inspection snapshots local run targets and input
        declarations before transformation. Direct provider-mocked unit runs
        receive a test-only location only when their local target gained a
        required input and neither global nor run variables already set it.
        Unknown run targets are rejected rather than rewritten.
        Examples run example, provider-cleanup, common after the module calls
        have settled. Standalone test-module directories run module-call,
        variable-order, provider-cleanup and an optional consumer test profile,
        deepest first.
        Their module calls receive only missing required locations; authored
        locations, optional defaults and telemetry opt-outs remain unchanged.
        Empty modtm test mocks are retired only when selected targets no
        longer require their provider. Unchanged dependencies retain the
        mock, its proven source requirement and same-provider module bindings,
        including known local callers and example wrappers, without restoring
        unrelated unused requirements. Proven same-provider aliases and local
        literal source client defaults retain their authored values.
        Telemetry references and empty random mocks in direct unit tests
        with no remaining random provider usage are migrated. Custom mocks
        that cannot be removed safely fail with an actionable error. The
        final call removes '*.tf.mptfbackup' files.

        Several of the vendored configs (e.g. order_resource_attrs) read
        provider schemas, so mapotf shells out to 'terraform init' +
        'terraform providers schema'. mapotf locates 'terraform' by name on
        PATH, but GitHub-hosted runners no longer ship terraform on PATH (it
        was removed from the images). The engine therefore resolves the pinned
        terraform via Resolve-AvmTool and prepends its directory to PATH for
        the mapotf subprocess; environment variables propagate to mapotf's own
        terraform grandchild, so the schema reads succeed against the managed
        binary. A terraform that cannot be resolved (AvmToolException)
        propagates so the chain reports 'skipped', matching missing-mapotf.

        File-hash snapshots taken before and after the transform populate the
        'Changed' field (relative paths of every '*.tf' or '*.tftest.hcl'
        file added, removed or modified).

        Drift mode (-CheckDrift, used by pr-check): mapotf has no dry-run, so
        the transform still runs and any 'Changed' file becomes a Status='fail'
        Issue. The transformed content is then rolled back, so drift mode leaves
        the working copy byte-identical. The contract is "a module that already
        ran pre-commit has nothing for mapotf to change"; a non-empty change set
        in CI therefore means the author did not run pre-commit, and pr-check
        flags it.

        Root and local-module targets finish before module calls and examples
        inspect their inputs. Each group runs through the bounded Invoke-AvmParallel
        scheduler. Mapotf uses each target's working-directory providers rather
        than TF_PLUGIN_CACHE_DIR, allowing independent targets and separate AVM
        processes to run without racing Terraform's shared provider cache.

        mapotf exit codes: 0 = success. A transform failure caused by a
        transient network error, such as a provider download timeout, is retried
        with the shared network retry policy; other failures and retry exhaustion surface as
        AvmProcessException. A missing mapotf binary (AvmToolException) or a
        missing config bundle (AvmConfigurationException) propagates so the
        composition chain reports the step as 'skipped' on an unconfigured
        workstation.

    .PARAMETER Context
        Module context produced by Get-AvmModuleContext. Must have
        Ecosystem='terraform'.

    .PARAMETER AllowPathFallback
        Pass through to Resolve-AvmTool.

    .PARAMETER CheckDrift
        When set, treat any file mapotf changed as a failure (Status='fail'
        with one Issue per changed file) instead of a silent fix. Used by the
        pr-check chain.

    .PARAMETER ThrottleLimit
        Maximum number of independent root, module, or example targets to
        transform at once. Defaults to one for direct engine calls.

    .OUTPUTS
        pscustomobject with Engine, Tool, ToolPath, ToolSource, Status,
        FilesProcessed, Changed, Issues.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    [OutputType([pscustomobject])]
    param(
        [Parameter(Mandatory)]
        $Context,

        [switch] $AllowPathFallback,

        [switch] $CheckDrift,

        [ValidateRange(1, 32)]
        [int] $ThrottleLimit = 1
    )

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

    if ($Context.Ecosystem -ne 'terraform') {
        throw [System.ArgumentException]::new(
            "Invoke-AvmTerraformTransform requires a terraform context (got Ecosystem='$($Context.Ecosystem)').")
    }

    $tool = Resolve-AvmTool -Name 'mapotf' -ModuleRoot $Context.Root -AllowPathFallback:$AllowPathFallback
    $profileDirs = @{
        common              = Resolve-AvmMapotfConfigDir -Root $Context.Root -ProfileName 'common'
        module              = Resolve-AvmMapotfConfigDir -Root $Context.Root -ProfileName 'module'
        root                = Resolve-AvmMapotfConfigDir -Root $Context.Root -ProfileName 'root'
        'module-call'       = Resolve-AvmMapotfConfigDir -Root $Context.Root -ProfileName 'module-call'
        'variable-order'    = Resolve-AvmMapotfConfigDir -Root $Context.Root -ProfileName 'variable-order'
        example             = Resolve-AvmMapotfConfigDir -Root $Context.Root -ProfileName 'example'
        'provider-cleanup'  = Resolve-AvmMapotfConfigDir -Root $Context.Root -ProfileName 'provider-cleanup'
        'location-inspect'  = Resolve-AvmMapotfConfigDir -Root $Context.Root -ProfileName 'location-inspect'
        'unit-test-inspect' = Resolve-AvmMapotfConfigDir -Root $Context.Root -ProfileName 'unit-test-inspect'
        'unit-test'         = Resolve-AvmMapotfConfigDir -Root $Context.Root -ProfileName 'unit-test'
        test                = Resolve-AvmMapotfConfigDir -Root $Context.Root -ProfileName 'test' -Optional
    }
    $targets = @(Get-AvmTerraformTransformTarget -Root $Context.Root)
    Write-AvmLog ("transform: discovered {0} target(s)" -f $targets.Count) -Level Verbose | Out-Null

    $beforeFiles = Get-AvmTerraformFile -Root $Context.Root
    Write-AvmLog ("transform: discovered {0} terraform file(s)" -f $beforeFiles.Count) -Level Verbose | Out-Null

    if (-not $PSCmdlet.ShouldProcess($Context.Root, ("mapotf transform across {0} scoped target(s)" -f $targets.Count))) {
        return [pscustomobject][ordered]@{
            Engine         = 'terraform'
            Tool           = ('{0}/{1}' -f $tool.Name, $tool.Version)
            ToolPath       = $tool.Path
            ToolSource     = $tool.Source
            Status         = 'skipped'
            FilesProcessed = $beforeFiles.Count
            Changed        = @()
            Issues         = @()
        }
    }

    $before = @{}
    foreach ($f in $beforeFiles) {
        $before[$f.FullName] = (Get-FileHash -LiteralPath $f.FullName -Algorithm SHA256).Hash
    }

    # Drift mode must not mutate the caller's working copy. mapotf has no
    # dry-run, so snapshot Terraform and test files up front and roll back in the finally -
    # drift is still computed from the post-transform tree, but the tree is left
    # byte-identical even if mapotf throws part-way through.
    $snapshot = $null
    $legacyIssues = @()
    if ($CheckDrift) {
        Write-AvmLog ("transform: check-drift mode; snapshotting {0} file(s)" -f $beforeFiles.Count) -Level Verbose | Out-Null
        $snapshot = Get-AvmFileSnapshot -Path @($beforeFiles | ForEach-Object { $_.FullName })
    }

    $inspectionRoot = Join-Path ([System.IO.Path]::GetTempPath()) ('avm-provider-inspection-' + [guid]::NewGuid().ToString('N'))
    try {
        # mapotf reads provider schemas (order_resource_attrs et al.) by shelling
        # out to terraform, which it finds by name on PATH. GitHub-hosted runners
        # no longer ship terraform on PATH, so resolve the pinned terraform the
        # same way as mapotf (managed cache, not a stray PATH binary) and prepend
        # its directory to PATH for the mapotf subprocess. The override propagates
        # to mapotf's terraform grandchild. A missing terraform throws
        # AvmToolException, which the chain surfaces as 'skipped' just like a
        # missing mapotf binary.
        $terraform = Resolve-AvmTool -Name 'terraform' -ModuleRoot $Context.Root -AllowPathFallback:$AllowPathFallback
        Write-AvmLog ("transform: resolved terraform dependency at {0}" -f $terraform.Path) -Level Verbose | Out-Null
        $mapotfEnv = New-AvmToolPathEnvironment `
            -ToolPath $terraform.Path `
            -ToolName 'terraform'
        $mapotfEnv.TF_PLUGIN_CACHE_DIR = $null
        $mapotfEnv.MAPOTF_PROVIDER_SCHEMA_CACHE_DIR = Join-Path (Get-AvmFolder -Kind Cache) 'mapotf-provider-schema'
        $null = New-Item `
            -ItemType Directory `
            -Path $mapotfEnv.MAPOTF_PROVIDER_SCHEMA_CACHE_DIR `
            -Force `
            -ErrorAction Stop

        $transformOptions = [pscustomobject]@{
            ToolPath       = $tool.Path
            TerraformPath  = $terraform.Path
            ProfileDirs    = $profileDirs
            EnvVars        = $mapotfEnv
            InspectionRoot = $inspectionRoot
        }
        $moduleTargets = @($targets | Where-Object { $_.Scope -in @('root', 'module') })
        $testTargets = @($targets | Where-Object { $_.Scope -eq 'test' })
        $exampleTargets = @($targets | Where-Object { $_.Scope -eq 'example' })
        $unitTestTargets = @($moduleTargets + $testTargets + $exampleTargets)
        $newLocationTargets = @(
            foreach ($target in $moduleTargets | Where-Object { $_.Profiles -notcontains 'root' }) {
                if ($null -eq (Get-AvmTerraformLocationInspection -Path $target.Path -Options $transformOptions)) {
                    $target
                }
            }
        )
        $unitSnapshots = @(Get-AvmTerraformUnitTestSnapshot -Root $Context.Root `
                -ModuleTargets $unitTestTargets -Options $transformOptions)

        Invoke-AvmParallel `
            -InputObject $moduleTargets `
            -FunctionName 'Invoke-AvmMapotfTransformTarget' `
            -Argument $transformOptions `
            -ThrottleLimit $ThrottleLimit
        $moduleCallTargets = @($moduleTargets | ForEach-Object {
                $relativePath = [System.IO.Path]::GetRelativePath($Context.Root, $_.Path)
                [pscustomobject]@{
                    Path     = $_.Path
                    Scope    = $_.Scope
                    Depth    = @($relativePath -split '[\\/]').Count
                    Profiles = @('module-call', 'variable-order', 'common')
                }
            })
        if ($moduleCallTargets.Count -gt 0) {
            foreach ($depthGroup in @($moduleCallTargets | Group-Object Depth | Sort-Object { [int]$_.Name } -Descending)) {
                Invoke-AvmParallel `
                    -InputObject @($depthGroup.Group) `
                    -FunctionName 'Invoke-AvmMapotfTransformTarget' `
                    -Argument $transformOptions `
                    -ThrottleLimit $ThrottleLimit
            }
        }
        if ($exampleTargets.Count -gt 0) {
            Invoke-AvmParallel `
                -InputObject $exampleTargets `
                -FunctionName 'Invoke-AvmMapotfTransformTarget' `
                -Argument $transformOptions `
                -ThrottleLimit $ThrottleLimit
        }
        if ($testTargets.Count -gt 0) {
            $testDepthGroups = @($testTargets | Group-Object {
                    @([System.IO.Path]::GetRelativePath($Context.Root, $_.Path) -split '[\\/]').Count
                } | Sort-Object { [int]$_.Name } -Descending)
            foreach ($depthGroup in $testDepthGroups) {
                Invoke-AvmParallel `
                    -InputObject @($depthGroup.Group) `
                    -FunctionName 'Invoke-AvmMapotfTransformTarget' `
                    -Argument $transformOptions `
                    -ThrottleLimit $ThrottleLimit
            }
        }
        Invoke-AvmTerraformUnitTestMigration -Root $Context.Root -ModuleTargets $unitTestTargets `
            -Snapshots $unitSnapshots -Options $transformOptions
        Set-AvmTelemetryTagLintDirective -Targets $moduleTargets
        if ($newLocationTargets.Count -gt 0) {
            Set-AvmRequiredLocationLintDirective -Targets $newLocationTargets -Options $transformOptions
        }
        Write-AvmLog 'transform: mapotf scoped transforms completed' -Level Verbose | Out-Null

        foreach ($target in $targets) {
            $clean = Invoke-AvmProcess `
                -FilePath $tool.Path `
                -ArgumentList @('clean-backup', '--tf-dir', $target.Path) `
                -WorkingDirectory $target.Path `
                -EnvVars $mapotfEnv `
                -IgnoreExitCode
            if ($clean.ExitCode -ne 0) {
                $message = Add-AvmProcessFailureDetail `
                    -Message ('mapotf clean-backup exited with code {0} for {1} target {2}.' -f $clean.ExitCode, $target.Scope, $target.Path) `
                    -StdOut $clean.StdOut `
                    -StdErr $clean.StdErr
                throw [AvmProcessException]::new(
                    $message)
            }
        }
        Write-AvmLog 'transform: mapotf clean-backup completed' -Level Verbose | Out-Null

        $afterFiles = Get-AvmTerraformFile -Root $Context.Root
        $seen = New-Object 'System.Collections.Generic.HashSet[string]'
        $changed = New-Object System.Collections.Generic.List[string]
        foreach ($f in $afterFiles) {
            $null = $seen.Add($f.FullName)
            $rel = [System.IO.Path]::GetRelativePath($Context.Root, $f.FullName)
            $hash = (Get-FileHash -LiteralPath $f.FullName -Algorithm SHA256).Hash
            if (-not $before.ContainsKey($f.FullName)) {
                $changed.Add($rel)
            }
            elseif ($before[$f.FullName] -ne $hash) {
                $changed.Add($rel)
            }
        }
        foreach ($key in $before.Keys) {
            if (-not $seen.Contains($key)) {
                $changed.Add([System.IO.Path]::GetRelativePath($Context.Root, $key))
            }
        }
        $legacyIssues = @(Get-AvmRemainingModtmIssue -Root $Context.Root -Targets @($moduleTargets + $exampleTargets + $testTargets))
    }
    finally {
        try {
            if ($null -ne $snapshot) {
                Write-AvmLog 'transform: restoring terraform snapshot after drift check' -Level Verbose | Out-Null
                $current = @(Get-AvmTerraformFile -Root $Context.Root | ForEach-Object { $_.FullName })
                Restore-AvmFileSnapshot -Snapshot $snapshot -CurrentPath $current
            }
        }
        finally {
            if (Test-Path -LiteralPath $inspectionRoot) {
                Remove-Item -LiteralPath $inspectionRoot -Recurse -Force -ErrorAction Stop -ProgressAction SilentlyContinue
            }
        }
    }

    $status = 'pass'
    $issues = New-Object System.Collections.Generic.List[object]
    foreach ($legacyIssue in $legacyIssues) {
        $status = 'fail'
        $issues.Add($legacyIssue)
    }
    if ($CheckDrift -and $changed.Count -gt 0) {
        $status = 'fail'
        foreach ($rel in $changed) {
            $issues.Add([pscustomobject][ordered]@{
                    File     = $rel
                    Line     = 0
                    Column   = 0
                    Severity = 'error'
                    Code     = 'avm.tf.mapotf-drift'
                    Message  = ("mapotf transform modified '{0}'; run 'avm pre-commit -Ecosystem terraform' and commit the result." -f $rel)
                })
        }
        Write-AvmLog ("transform: completed; processed={0}; changed={1}; status={2}" -f $beforeFiles.Count, $changed.Count, $status) -Level Verbose | Out-Null
    }

    return [pscustomobject][ordered]@{
        Engine         = 'terraform'
        Tool           = ('{0}/{1}' -f $tool.Name, $tool.Version)
        ToolPath       = $tool.Path
        ToolSource     = $tool.Source
        Status         = $status
        FilesProcessed = $beforeFiles.Count
        Changed        = $changed.ToArray()
        Issues         = $issues.ToArray()
    }
}

# SIG # Begin signature block
# MIInQQYJKoZIhvcNAQcCoIInMjCCJy4CAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCC53lF/9dV5YVQM
# trmf9fANhlkkWIn+HmJ7ebHaaFsoqqCCDLowggX1MIID3aADAgECAhMzAAACHU0Z
# 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
# KoZIhvcNAQkEMSIEIMlvqIlfRRG+cl7xW2UIK2bxsloLCu03hXVCnXApFHnzMEIG
# CisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8AcwBvAGYAdKEagBhodHRwOi8v
# d3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEBBQAEggEAwUNO5OK7W42fix52
# XRM977cW2vs7J2iCTxoQ4KfJwE7mR/1I7s+mz73IlHo1dcIkJPUs+1Wy3naPQqXP
# nlPUwyfQjuxo1IkSAaI/qXFzmrEnvtzM/8i+laGDt+ajj/2Ofi2PVegG0Lj0JzLx
# KS42MV+uWyYmGkPnjW/qoS/RDTox8qoK/j6D5QYCyVra7Vbqy7IQ5TB2EAeyiHf8
# ICMDFD6MBWp3zTPfYXMHlw8SknLxYMNI+1qs7QQKO+SwbK3+nmmZ7VPc+qhSy6Cw
# k1vF0Jo9/QA/CVuV4MCoBFmtS0DzDXY0GGql7JkgiJnrARdL8SpFM1ZWdHS6IFPV
# JV2v36GCF60wghepBgorBgEEAYI3AwMBMYIXmTCCF5UGCSqGSIb3DQEHAqCCF4Yw
# gheCAgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFaBgsqhkiG9w0BCRABBKCCAUkEggFF
# MIIBQQIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFlAwQCAQUABCAVME7ddcR50vfY
# s3wv8Mfjtp07jmRFQl3dfI/7AUMWTQIGaq6xsAuLGBMyMDI2MTAxNTE5MDY0MS45
# OTNaMASAAgH0oIHZpIHWMIHTMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGlu
# Z3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBv
# cmF0aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJlbGFuZCBPcGVyYXRpb25zIExp
# bWl0ZWQxJzAlBgNVBAsTHm5TaGllbGQgVFNTIEVTTjo0MzFBLTA1RTAtRDk0NzEl
# MCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZaCCEfswggcoMIIF
# EKADAgECAhMzAAACHUvAkoc4hX45AAEAAAIdMA0GCSqGSIb3DQEBCwUAMHwxCzAJ
# BgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25k
# MR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jv
# c29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMB4XDTI1MDgxNDE4NDgzM1oXDTI2MTEx
# MzE4NDgzM1owgdMxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAw
# DgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24x
# LTArBgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5kIE9wZXJhdGlvbnMgTGltaXRlZDEn
# MCUGA1UECxMeblNoaWVsZCBUU1MgRVNOOjQzMUEtMDVFMC1EOTQ3MSUwIwYDVQQD
# ExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNlMIICIjANBgkqhkiG9w0BAQEF
# AAOCAg8AMIICCgKCAgEAorSgaAA8oOl4ph574zw29egUN8DDepRHLX8FM1zHNJmX
# G6KrSqUKwzcKafopuYdPTETTCvb9aJfESuAU0iGNUFI/D6R0kvdfpe2oPX+E3sbT
# QvGi4JPH5qdIYUaJ45V/4bqe8eNvbWzpC+ZKjH193DeiI1XAI918JoQmBhlEXo/T
# on1721luZJgincsf5LjMY3jX84WyXUSX3dsS7h/7xVI+w1yjg7pa+0y3o/me2Tsv
# 6UJUdSTQap5ORGSfCnclnP1z3IiiWIWr3Vo7aIPWsgJzq3m5GxpxUHCQk8qzUhk5
# 0y/uB+LGE3WIK2C77iy9iFsSfSLUnyMEzGRDW9mXHT4PH7Ozz6CHqQEiNvwcHqlv
# lCh1pHQh1NXQSAqOoVBs5mi6easf6yxWTfe5DrR79503r8pU6VqC2Y9XMRU4wH9Q
# bYXYsIUZ33Jmndy22W1LBDAbxBPQHCBlncGDU3BgdhVUVLe80mggFO98FdkWho67
# w4kPdCTRkvdvkY8PrQYE/nQjHXCa0g7LcMttZb6ejMHfQ+tUWXv6+nZ4Ynkr2Oka
# xclFCw4RIYNMWD26AWbQj/WEdzga18fKtw66L5gzXPza6jFBfPJeKE3H8QAuwpir
# mH4ms+5nUjNNQOmNgqJn0U1+3Yn7ClswD79YN0r3fdbYBMDApBZJpNlK7q7HXRsC
# AwEAAaOCAUkwggFFMB0GA1UdDgQWBBSEWfBxNEamZtXm8gl92Yq80jfxXTAfBgNV
# HSMEGDAWgBSfpxVdAF5iXYP05dJlpxtTNRnpcjBfBgNVHR8EWDBWMFSgUqBQhk5o
# dHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNyb3NvZnQlMjBU
# aW1lLVN0YW1wJTIwUENBJTIwMjAxMCgxKS5jcmwwbAYIKwYBBQUHAQEEYDBeMFwG
# CCsGAQUFBzAChlBodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRz
# L01pY3Jvc29mdCUyMFRpbWUtU3RhbXAlMjBQQ0ElMjAyMDEwKDEpLmNydDAMBgNV
# HRMBAf8EAjAAMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMA4GA1UdDwEB/wQEAwIH
# gDANBgkqhkiG9w0BAQsFAAOCAgEAkdweB4yxvLspLKq0D+miyD4Q0EcxVFpNZuJx
# iR54gWRkeTDDuymNeB03JhlsBpbwSYJ5uZSgDBCvwHED2VL8lJpFlOprJzxsXWC2
# NTfA+O+PO5Fk5jw6LHh6jeBADDEdQAx3Hqi7Zm0JwvQ93z5f6dtxkm29WqOcHYXR
# XfAQwy1hSrLXyfeblqR66jpP/9n0fCkWU4ggsUjQpQ2Ngj1DV09J4Y3y7p9Nd81+
# Xs6qYo++7RKm8qiB/5NDeigOLjlAeFgiEXIRUJW+mJyqpQw+OORlaqcFjR8Hu0G+
# /7bMdek68YX+kPpDBk7Ue+I/xgiYJ1xcDRBn/vczLtN72+RIlD4UgXYLuBSCk//p
# DEPX5z39Cr+rkc6E4Y28FPk4BhloAyvp628P4xfElQY8TcxraUbZShypocE6ny95
# D1K1BkltZmrHVKCxmglnuOlM15NKIrXFlXCzdqpCtIwQ417wNAVF/QDPvzzbumPd
# Ti6fb0tLbScYobV6zvbBsMsKEME4Tj1b9oIXC8dybJq4nbboEXYpRwi1QAbpSNrn
# +PxGW9uf1q63FnMJu4gm3Oh63njW/iVf723quzyHrSijWMgY0HiRiHQi0Jyu0h8M
# dhRUp7mxbmLQckPiOFwAlIaUN/k725y/aLWpkRU6fqmLlEOyH5WpyLd23AYy9r8v
# +Qoba6swggdxMIIFWaADAgECAhMzAAAAFcXna54Cm0mZAAAAAAAVMA0GCSqGSIb3
# 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
# bWl0ZWQxJzAlBgNVBAsTHm5TaGllbGQgVFNTIEVTTjo0MzFBLTA1RTAtRDk0NzEl
# MCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZaIjCgEBMAcGBSsO
# AwIaAxUAuoO+BKbfXzqyfi9GLEdWHkCLeT+ggYMwgYCkfjB8MQswCQYDVQQGEwJV
# UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE
# ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGlt
# ZS1TdGFtcCBQQ0EgMjAxMDANBgkqhkiG9w0BAQsFAAIFAO57dDUwIhgPMjAyNjEw
# MTUxNTQ4MDVaGA8yMDI2MTAxNjE1NDgwNVowdDA6BgorBgEEAYRZCgQBMSwwKjAK
# AgUA7nt0NQIBADAHAgEAAgI6MDAHAgEAAgITbzAKAgUA7nzFtQIBADA2BgorBgEE
# AYRZCgQCMSgwJjAMBgorBgEEAYRZCgMCoAowCAIBAAIDB6EgoQowCAIBAAIDAYag
# MA0GCSqGSIb3DQEBCwUAA4IBAQAzSqUhzUz6vtZViC8Bs10EZcXah2C3vdodo7qi
# Ji1REdkUg+ZqADnwDYxu0Hij6dQOGhdFT7gOkxXnBDahrWL2rNAxKas5OPZuVyxz
# 4BiC85U2roTz/unoEeX9X2s+eTL7WTkjq6uSpkKU3P/mcqxohGsJUgSCPhm5Tgye
# iSYLEp19bDj6arpez+/WXhd8uK5mhg2KKzV+Kx7QDrdlsdKM/9IEjv+4py27Q2sd
# DaOAtILi/u0vHSbKHUue5XOFz8gt1q/vXP62bZe2paj8w5qMruXOvV6c+jKaQPAA
# mWANKt8z4HpCgYEEURbSSrin3k5eiF2I5Lc2GXx2mQSn6wN1MYIEDTCCBAkCAQEw
# gZMwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcT
# B1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UE
# AxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTACEzMAAAIdS8CShziFfjkA
# AQAAAh0wDQYJYIZIAWUDBAIBBQCgggFKMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0B
# CRABBDAvBgkqhkiG9w0BCQQxIgQgVGfmwis7vhvy63DReqKFBU5n5ZYJfo41YaCw
# FlpB6oYwgfoGCyqGSIb3DQEJEAIvMYHqMIHnMIHkMIG9BCCxtpXMXEiLJzrqM77e
# p4rTNwrMOj6gpWN9hZvpj5QFUTCBmDCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1w
# IFBDQSAyMDEwAhMzAAACHUvAkoc4hX45AAEAAAIdMCIEIGGtrLikVdvCzu7AQY4+
# UybqDtd0J8aMnjt7M078tVzlMA0GCSqGSIb3DQEBCwUABIICAE/pCTdeyO3E712N
# yqGtu7qTe/f89KMNnEMFdHs55tbXLog7kaCefPt8/IBwAHIkB7uK86s7ODcMh/Np
# qWwigNzwEgBJ2wEDhhTIxYDcEfcz7xUOGWDnu6l4K96cTPtEsyus5uoIxeNcia2g
# oZDFtNq394MX/vC2U+MrxDRbhLrqt6qx+M3G3EVg3CZclDiZYsLjRWAHpcYG8xBw
# kqTkrEuCN1T5wlC971/GhYrR7E+pnr/oZZJbn7rFHtVCTOmwqihDUzMz3s1v7SRF
# Wui9nIpA0zkCg4IF7ZeYT9atGxUzNtWPABDng8Wr/Q4Ic71HvqnIhL6L1n3ws5DM
# ciAqcH7Tyj8y8F2zdvpg/S2jl390n2m131LTwpS6GNDXaJOtg4zSxGMhOy4uQcpL
# 3JcV/mpow3p482Nq21JqqjbDAEOm+xjMm6t/P/EvzaNPJ2ybNpbD1Ju8v/W79zu2
# evIyX1T0cUsWTX36iwZ4HnAaq36qZGK93QMKrjgglE+FkJpEmQ8qrf26NgimJEh9
# p65ya+NqITlQsrw8S58P7L8QQwKx7NUDCI/fR9OcxuhNktVgbg3jBECIShgFi8j7
# jggmSBjhkLwyo5THoDoaXV4/VV4l7aEADtCHGOc69n4nWF51nMc2EkUpEBv1r6fW
# MnPrRRZAxjARwxv5+13dfMwgvfIU
# SIG # End signature block