Engines/ManagedFiles/Merge-AvmFileLine.ps1

function Merge-AvmFileLine {
    <#
    .SYNOPSIS
        Line-level merge: ensure every required line is present and every removed
        line is absent, preserving all other existing lines and their order.

    .DESCRIPTION
        Pure function (no I/O). Matching is exact on the trimmed line text and
        case-sensitive. Required lines that are already present are left in place;
        genuinely missing ones are appended at the end in declaration order.
        Every occurrence of a removed line is deleted. Blank lines are never
        removed. The operation is idempotent: re-running over its own output
        reports no change.

        A line supplied in both Required and Removed is a contradiction and
        throws, so a bad spec fails loudly rather than resolving silently.

    .OUTPUTS
        pscustomobject with Lines (string[]), Added (string[]),
        Removed (string[]), and Changed (bool).
    #>

    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param(
        [string[]] $ExistingLine = @(),
        [string[]] $Required = @(),
        [string[]] $Removed = @()
    )

    Set-StrictMode -Version 3.0

    $existing = @($ExistingLine)
    $required = @($Required)
    $removed = @($Removed)

    $removedSet = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal)
    foreach ($item in $removed) {
        if ($null -eq $item) { continue }
        $trimmed = ([string]$item).Trim()
        if ($trimmed.Length -eq 0) { continue }
        [void]$removedSet.Add($trimmed)
    }
    foreach ($item in $required) {
        if ($null -eq $item) { continue }
        $trimmed = ([string]$item).Trim()
        if ($removedSet.Contains($trimmed)) {
            throw [System.ArgumentException]::new(
                "Line '$trimmed' appears in both 'required' and 'removed'; a managed-line spec entry cannot require and remove the same line.")
        }
    }

    $kept = New-Object System.Collections.Generic.List[string]
    $removedLines = New-Object System.Collections.Generic.List[string]
    $presentTrimmed = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal)

    foreach ($line in $existing) {
        $value = if ($null -eq $line) { '' } else { [string]$line }
        $trimmed = $value.Trim()
        if ($trimmed.Length -gt 0 -and $removedSet.Contains($trimmed)) {
            $removedLines.Add($value)
            continue
        }
        $kept.Add($value)
        if ($trimmed.Length -gt 0) { [void]$presentTrimmed.Add($trimmed) }
    }

    $addedLines = New-Object System.Collections.Generic.List[string]
    foreach ($item in $required) {
        if ($null -eq $item) { continue }
        $value = [string]$item
        $trimmed = $value.Trim()
        if ($trimmed.Length -eq 0) { continue }
        if ($presentTrimmed.Contains($trimmed)) { continue }
        $kept.Add($value)
        $addedLines.Add($value)
        [void]$presentTrimmed.Add($trimmed)
    }

    return [pscustomobject][ordered]@{
        Lines   = $kept.ToArray()
        Added   = $addedLines.ToArray()
        Removed = $removedLines.ToArray()
        Changed = (($addedLines.Count -gt 0) -or ($removedLines.Count -gt 0))
    }
}

function Get-AvmManagedLineSpec {
    <#
    .SYNOPSIS
        Collect and stack the line-managed-file spec across a repository's
        managed-file groups.

    .DESCRIPTION
        Reads the 'managedLines' key of each file group's '_config.json' -- the
        same file that carries 'deletedFiles' -- in the same order
        Build-AvmManagedFilesMap applies the groups. Both are instructions about
        how to mutate a target repository rather than content copied into it, so
        they live in the group's reserved config file and can never be mistaken
        for a file to sync.

        Each 'managedLines' value is a flat JSON map of forward-slash relative
        path -> { required: [...], removed: [...] }.

        Entries stack per line with last-writer-wins: a later group that
        requires a line cancels an earlier group that removed it, and vice
        versa, mirroring how later group files win over earlier ones. Within a
        single group a line in both 'required' and 'removed' is a contradiction
        and throws.

    .OUTPUTS
        An ordered hashtable keyed by relative path; each value is a
        pscustomobject with Required (string[]) and Removed (string[]).
    #>

    [CmdletBinding()]
    [OutputType([System.Collections.Specialized.OrderedDictionary])]
    param(
        [AllowEmptyString()]
        [string] $BaseDir = '',

        [string[]] $FileGroups = @()
    )

    Set-StrictMode -Version 3.0

    $spec = [ordered]@{}

    if ([string]::IsNullOrWhiteSpace($BaseDir)) { return $spec }

    foreach ($fileGroup in $FileGroups) {
        if ([string]::IsNullOrWhiteSpace($fileGroup)) { continue }
        $groupName = [string]$fileGroup

        $config = Get-AvmManagedFileGroupConfig -BaseDir $BaseDir -FileGroup $groupName
        if ($null -eq $config) { continue }
        if (-not ($config.PSObject.Properties.Name -contains 'managedLines') -or -not $config.managedLines) { continue }

        $parsed = $config.managedLines
        if ($null -eq $parsed) { continue }

        foreach ($property in $parsed.PSObject.Properties) {
            $targetPath = ($property.Name -replace '\\', '/').Trim()
            if ([string]::IsNullOrWhiteSpace($targetPath)) { continue }

            $entry = $property.Value
            if ($null -eq $entry) { continue }

            $required = @()
            $removed = @()
            if ($entry.PSObject.Properties.Name -contains 'required' -and $entry.required) {
                $required = @($entry.required)
            }
            if ($entry.PSObject.Properties.Name -contains 'removed' -and $entry.removed) {
                $removed = @($entry.removed)
            }

            $fileRequired = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal)
            foreach ($r in $required) {
                if ($null -eq $r) { continue }
                $t = ([string]$r).Trim()
                if ($t.Length -gt 0) { [void]$fileRequired.Add($t) }
            }
            foreach ($r in $removed) {
                if ($null -eq $r) { continue }
                $t = ([string]$r).Trim()
                if ($t.Length -gt 0 -and $fileRequired.Contains($t)) {
                    throw [System.InvalidOperationException]::new(
                        "Managed-line spec for file group '$groupName' lists line '$t' for '$targetPath' in both 'required' and 'removed'.")
                }
            }

            if (-not $spec.Contains($targetPath)) {
                $spec[$targetPath] = [pscustomobject]@{
                    Required = (New-Object System.Collections.Generic.List[string])
                    Removed  = (New-Object System.Collections.Generic.List[string])
                }
            }
            $bucket = $spec[$targetPath]

            foreach ($r in $required) {
                if ($null -eq $r) { continue }
                $value = [string]$r
                $trimmed = $value.Trim()
                if ($trimmed.Length -eq 0) { continue }
                for ($i = $bucket.Removed.Count - 1; $i -ge 0; $i--) {
                    if ($bucket.Removed[$i].Trim() -ceq $trimmed) { $bucket.Removed.RemoveAt($i) }
                }
                if (-not ($bucket.Required | Where-Object { $_.Trim() -ceq $trimmed })) {
                    $bucket.Required.Add($value)
                }
            }
            foreach ($r in $removed) {
                if ($null -eq $r) { continue }
                $value = [string]$r
                $trimmed = $value.Trim()
                if ($trimmed.Length -eq 0) { continue }
                for ($i = $bucket.Required.Count - 1; $i -ge 0; $i--) {
                    if ($bucket.Required[$i].Trim() -ceq $trimmed) { $bucket.Required.RemoveAt($i) }
                }
                if (-not ($bucket.Removed | Where-Object { $_.Trim() -ceq $trimmed })) {
                    $bucket.Removed.Add($value)
                }
            }
        }
    }

    $result = [ordered]@{}
    foreach ($key in $spec.Keys) {
        $result[$key] = [pscustomobject]@{
            Required = $spec[$key].Required.ToArray()
            Removed  = $spec[$key].Removed.ToArray()
        }
    }
    return $result
}

function Get-AvmManagedLinePlan {
    <#
    .SYNOPSIS
        Compute the per-file line-merge plan for a resolved line-managed-file
        spec against a working tree, without writing anything.

    .DESCRIPTION
        For each spec path it reads the current file (if any), detects the
        dominant line ending, and computes the merged content via
        Merge-AvmFileLine. A missing file is treated as empty so required lines
        create it (new files use LF); an existing file keeps its own line ending.
        All content is read and written as UTF-8 without a BOM.

    .OUTPUTS
        An array of pscustomobject, one per spec path, with Path, Full, Existed,
        Changed, AddedLines, RemovedLines, and NewText.
    #>

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

        [Parameter(Mandatory)]
        [System.Collections.Specialized.OrderedDictionary] $Spec
    )

    Set-StrictMode -Version 3.0

    $utf8NoBom = [System.Text.UTF8Encoding]::new($false)
    $plans = New-Object System.Collections.Generic.List[object]

    foreach ($targetPath in $Spec.Keys) {
        $entry = $Spec[$targetPath]
        $full = Join-Path $Root ($targetPath.Replace('/', [System.IO.Path]::DirectorySeparatorChar))

        $existed = Test-Path -LiteralPath $full -PathType Leaf
        $newline = "`n"
        $existingLines = @()
        if ($existed) {
            $text = $utf8NoBom.GetString([System.IO.File]::ReadAllBytes($full))
            if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) }
            if ($text.Contains("`r`n")) { $newline = "`r`n" }
            $existingLines = @([regex]::Split($text, "`r`n|`n|`r"))
            if ($existingLines.Count -gt 0 -and $existingLines[-1] -eq '') {
                if ($existingLines.Count -eq 1) {
                    $existingLines = @()
                }
                else {
                    $existingLines = @($existingLines[0..($existingLines.Count - 2)])
                }
            }
        }

        $merge = Merge-AvmFileLine -ExistingLine $existingLines -Required $entry.Required -Removed $entry.Removed

        $newText = ''
        if ($merge.Lines.Count -gt 0) { $newText = ($merge.Lines -join $newline) + $newline }

        $plans.Add([pscustomobject][ordered]@{
                Path         = $targetPath
                Full         = $full
                Existed      = $existed
                Changed      = $merge.Changed
                AddedLines   = $merge.Added
                RemovedLines = $merge.Removed
                NewText      = $newText
            })
    }

    return $plans.ToArray()
}

# SIG # Begin signature block
# MIInOgYJKoZIhvcNAQcCoIInKzCCJycCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDSPCETnCj6zqER
# QgkD4OVWNrss3NYMJZykTJ8dOYkTA6CCDMkwggYEMIID7KADAgECAhMzAAACHPrN
# xZvoL37EAAAAAAIcMA0GCSqGSIb3DQEBCwUAMFcxCzAJBgNVBAYTAlVTMR4wHAYD
# VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBD
# b2RlIFNpZ25pbmcgUENBIDIwMjQwHhcNMjYwNDE2MTg1OTQxWhcNMjcwNDE1MTg1
# OTQxWjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE
# BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYD
# VQQDExVNaWNyb3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IB
# DwAwggEKAoIBAQDVsZfgOKmM31HPfoWOoNEiw0SlCiIxUMC0I9NMWbucKOw/e9lP
# oAoehQVu6SG65V4EPzrYsnBnFPNoi4/HoOdjhz1qkrEt4I6tEcxXU6oOeY9zGveC
# /3iBeuhLYxM3M/PkcUoebF+Nednm8OkdSPoDu8imViHPQq/8CQUu0WRR4rE+dMRf
# rpVqfmNi2qWCX94T4MsepijGVkwE//tJg0ryAiYdHT34LSnlG/RSBZmQRGWZ5g8j
# qnKjRParSqMft1gvjuUTVgtWNZfgcLFSK5Wa0myrq8OPcgTGGsRgun+tnSS+IxDT
# xVsAPH1OzvPjwomguByhUe/OcvUN0D5Wmp7xAgMBAAGjggGqMIIBpjAOBgNVHQ8B
# Af8EBAMCB4AwHwYDVR0lBBgwFgYKKwYBBAGCN0wIAQYIKwYBBQUHAwMwHQYDVR0O
# BBYEFNoH7a2YDjOSwpkp6DHcmUS7J+0yMFQGA1UdEQRNMEukSTBHMS0wKwYDVQQL
# EyRNaWNyb3NvZnQgSXJlbGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxFjAUBgNVBAUT
# DTIzMDAxMis1MDc1NjkwHwYDVR0jBBgwFoAUf1k/VCHarU/vBeXmo9ctBpQSCDEw
# YAYDVR0fBFkwVzBVoFOgUYZPaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9w
# cy9jcmwvTWljcm9zb2Z0JTIwQ29kZSUyMFNpZ25pbmclMjBQQ0ElMjAyMDI0LmNy
# bDBtBggrBgEFBQcBAQRhMF8wXQYIKwYBBQUHMAKGUWh0dHA6Ly93d3cubWljcm9z
# b2Z0LmNvbS9wa2lvcHMvY2VydHMvTWljcm9zb2Z0JTIwQ29kZSUyMFNpZ25pbmcl
# MjBQQ0ElMjAyMDI0LmNydDAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4IC
# AQAUnEqhaRXe0T3hIJjvdQErEkrA/7bByjn6t5IArODkkRjzkYwtKMc2yYj2quaN
# rLutWw2YZcngKPy1b71YyDJQTy4NDRwaSh9Tw5thrk3NmcPrAHia5vtcBJ1CgtKK
# 7mQbIcQ22d/N3813ayCDDFewu1+jsZmX+r/aTEqaOM4TVxVtRSkuCy8nAXKuChOK
# Li/zA4XuH8iEYqIsj2YoNaeSxVmeGiERXpKdo3dDmYi0kO5w2D8VS4c3+9h6gElY
# BaAAg/dYErBg27qT3vv0zRDJhJufvCNylA8S7/+8H5E/PV5cng6na9VV/w9OV3qu
# uND6zdGa2EX38Glp50F9AIQk3p2xXmcvorDeM4XJ7UlWYBi6g80J1SSOQnInCYFE
# msfUNn3+1AaTJKSJL83quKArTac2pKhu0Yzzzrzo6HrsRiQKzpnRBb1/dMa6P3hz
# 75XbMRBctNsFhZC07WCmjExdLg2eHW5uV0TY8D5+6wozJf7vF3+WHkYPO85Z+BC6
# U4FkNbYNycZ9cE4j1tXRdyDCfml6c0HWPHjNVDObrv9lKt3qUqFpX38VCqVCyNOO
# 1UcXfQiVjJw32U2WUKZjt/neJKHEBsm9kFsLuWzkQ53+qcaSaytmsCnk2gOglrlD
# 5d3kKyvvAw+rzm0lT8K38P6PLxfZQHhu4W8dV7Av8N2ZmDCCBr0wggSloAMCAQIC
# EzMAAAA5O7Y3Gb8GHWcAAAAAADkwDQYJKoZIhvcNAQEMBQAwgYgxCzAJBgNVBAYT
# AlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYD
# VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xMjAwBgNVBAMTKU1pY3Jvc29mdCBS
# b290IENlcnRpZmljYXRlIEF1dGhvcml0eSAyMDExMB4XDTI0MDgwODIwNTQxOFoX
# DTM2MDMyMjIyMTMwNFowVzELMAkGA1UEBhMCVVMxHjAcBgNVBAoTFU1pY3Jvc29m
# dCBDb3Jwb3JhdGlvbjEoMCYGA1UEAxMfTWljcm9zb2Z0IENvZGUgU2lnbmluZyBQ
# Q0EgMjAyNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANgBnB7jOMeq
# lRYHNa265v4IY9fH8TKhemHfPINe1gpLaV3dhg324WwH06LcHbpnsBukCDNitryo
# 0dtS/EW6I/yEL/bLSY8hKpbfQuWusBPr9qazYcDxCW/qnjb5JsI1s8bNOg3bVATv
# QVL4tcf03aTycsz8QeCdM0l/yHRObJ9QqazM1r6VPEOJ7LL+uEEb73w6QCuhs89a
# 1uv1zerOYMnsneRRwCbpyW11IcggU0cRKDDq1pjVJzIbIF6+oiXXbReOsgeI8zu1
# FyQfK0fVkaya8SmVHQ/tOf23mZ4W9k0Ri22QW9p3UgSC5OUDktKxxcCmGL6tXLfO
# GSWHIIV4YrTJTT6PNty5REojHJuZHArkF9VnHTERWoTjAzfI3kP+5b4alUdhgAZ7
# ttOu1bVnXfHaqPYl2rPs20ji03LOVWsh/radgE17es5hL+t6lV0eVHrVhsssROWJ
# uz2MXMCt7iw7lFPG9LXKGjsmonn2gotGdHIuEg5JnJMJVmixd5LRlkmgYRZKzhxS
# CwyoGIq0PhaA7Y+VPct5pCHkijcIIDm0nlkK+0KyepolcqGm0T/GYQRMhHJlGOOm
# VQop36wUVUYklUy++vDWeEgEo4s7hxN6mIbf2MSIQ/iIfMZgJxC69oukMUXCrOC3
# SkE/xIkgpfl22MM1itkZ35nNXkMolU1lAgMBAAGjggFOMIIBSjAOBgNVHQ8BAf8E
# BAMCAYYwEAYJKwYBBAGCNxUBBAMCAQAwHQYDVR0OBBYEFH9ZP1Qh2q1P7wXl5qPX
# LQaUEggxMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMA8GA1UdEwEB/wQFMAMB
# Af8wHwYDVR0jBBgwFoAUci06AjGQQ7kUBU7h6qfHMdEjiTQwWgYDVR0fBFMwUTBP
# oE2gS4ZJaHR0cDovL2NybC5taWNyb3NvZnQuY29tL3BraS9jcmwvcHJvZHVjdHMv
# TWljUm9vQ2VyQXV0MjAxMV8yMDExXzAzXzIyLmNybDBeBggrBgEFBQcBAQRSMFAw
# TgYIKwYBBQUHMAKGQmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMv
# TWljUm9vQ2VyQXV0MjAxMV8yMDExXzAzXzIyLmNydDANBgkqhkiG9w0BAQwFAAOC
# AgEAFJQfOChP7onn6fLIMKrSlN1WYKwDFgAddymOUO3FrM8d7B/W/iQ6DxXsDn7D
# 5W4wMwYeLystcEqfkjz4NURRgazyMu5yRzQh4LqjA4tStTcJh1opExo7nn5PuPBY
# nbu0+THSuVHTe0VTTPVhily/piFrDo3axQ9P4C+Ol5yet+2gTfekICS5xS+cYfSI
# vgn0JksVBVMYVI5QFu/qhnLhsEFEUzG8fvv0hjgkO+lkpV9ty6GkN4vdnd7ya6Q6
# aR9y34aiM1qmxaxBi6OUnyNl6fkuun/diTFnYDLTppOkr/mg5WSfCiDVMNCxtj4w
# PKC5OmHm1DQIt/MNokbbH3UGsFP1QbzsLocuSqLCvH09Io3fDPTmscR9Y75G4qX7
# RTX8AdBPo0I6OEojf39zuFZt0qOHm65YWQE69cZM2ueE1MB05dNNgHK9gTE7zKvK
# /fg8B2qjW88MT/WF5V5uvZGtqa9FSL2RazArA+rDPuf6JGYz4HpgMZHB4S6szWSK
# YBv0VisCzfxgeU+dquXW9bd0auYlOB58DPcOYKdc3Se94g+xL4pcEhbB54JOgAkw
# YTu/9dLeH2pDqeJZAABVDWRQCaXfO5LgyKwKCLYXpigrZYCjUSBcr+Ve8PFWMhVT
# Ql0v4q8J/AUmQN5W4n101cY2L4A7GTQG1h32HHAvfQESWP0xghnHMIIZwwIBATBu
# MFcxCzAJBgNVBAYTAlVTMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24x
# KDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMjQCEzMAAAIc
# +s3Fm+gvfsQAAAAAAhwwDQYJYIZIAWUDBAIBBQCggZAwGQYJKoZIhvcNAQkDMQwG
# CisGAQQBgjcCAQQwLwYJKoZIhvcNAQkEMSIEIKaX6EOEf4XRjpPrpOnMx2F9MwrN
# 35qYTbmLLAYbuMflMEIGCisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8AcwBv
# AGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEBBQAE
# ggEAhZJV+31pufjHn6RVJQlVqQlqkGYe/tP7b/ZPOrlshl+HHQyDeIkhweny5JTg
# gb5yKupLaAfR8MHHKPA1ruMOM2E1BVE5zcZLoLHteMeuXb9m+0G1tzS4Xvi/gBsu
# N9xISfxhehSZqwS7x8isFImTlVWl4k0e3uo0hqu2cz0CAbs/e8V8cndCIZawcWS3
# aZUHKVCfxHHdgolH6rIiJDdtR65A/q7XYGz7oc1PaQssHE5ciShsjew1aR40/XMF
# FW+gAZVXD0dir0k9yegV2NpJqae/HhcuYLt5kq73tQt66J9ccrg3+ix36VQ3S6FJ
# CJHEIryX/af63IFBw9Js1MkIF6GCF5cwgheTBgorBgEEAYI3AwMBMYIXgzCCF38G
# CSqGSIb3DQEHAqCCF3AwghdsAgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFSBgsqhkiG
# 9w0BCRABBKCCAUEEggE9MIIBOQIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFlAwQC
# AQUABCBUWXJblHFegZOsvqGOKWowy0lqAi/XMeO4sXY90m/hugIGaoUw0E6KGBMy
# MDI2MDgyNzE2MDMyNy44NzVaMASAAgH0oIHRpIHOMIHLMQswCQYDVQQGEwJVUzET
# MBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMV
# TWljcm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1lcmlj
# YSBPcGVyYXRpb25zMScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046MzcwMy0wNUUw
# LUQ5NDcxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2WgghHt
# MIIHIDCCBQigAwIBAgITMwAAAh86cGnkojAulQABAAACHzANBgkqhkiG9w0BAQsF
# ADB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH
# UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQD
# Ex1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDAeFw0yNjAyMTkxOTM5NTFa
# Fw0yNzA1MTcxOTM5NTFaMIHLMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGlu
# Z3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBv
# cmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1lcmljYSBPcGVyYXRpb25zMScw
# JQYDVQQLEx5uU2hpZWxkIFRTUyBFU046MzcwMy0wNUUwLUQ5NDcxJTAjBgNVBAMT
# HE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2UwggIiMA0GCSqGSIb3DQEBAQUA
# A4ICDwAwggIKAoICAQDLO8XFOcfGqAqgiz0+AmQmFl3dZ0aTG4UFJkqqNdMHy28D
# aheCBs6ONufukye5x42CWkzgRIy9kE2VWwEntZ8ZkgyrykC0bIqsID7+6FxguseT
# Xf1Vwvm1D8104VmetoBJlJ4uGbuyJZUvXDx55nVh50ygLTzZ24WkQsnPpvRZv2kP
# c39f3bhLyHVtnHsa/W/86Vrftd+AfFveA+qN/EY+XGj5c/DPMXCYECb0arYb92dD
# JWtwzpyBrp4gfHlgY1UEpc4l4AGELrf2J4wrxTzTW+SM8XhV1dOOPrYjD080IbZq
# L8B+IF0RCdn269YXrGK6QIHipznKZcCS8jN30YAHnTJVN5Zzs6t/2YsqBGDquvDa
# d7934FFTwzvUcO3VoIyd93XWwvP8/SCFVJh21W8oGQTptGHyly+Fl4henVMVZF1v
# 6osOtirX8GFTiEhnf8nRdOg7yZYAJ0xy9CtDfbXaTn/cf3Lq3N/GCYKFjC+5mUCE
# +AJhmxMuMdvSUGmKiAFdiPAjUTqsWWBBZJm0eCwgeGJFmmQA+V7/98BKcE+gUL7O
# 9eWRDQwKeAcvo6rxNv2Y4jKrHA6Z/wi3a/fKUhLCNZES8qGdrpDAm7qh+6FjYxyt
# AbkiKM6uTNy/ULPlwtlYZoAJDDQP7eYCywwVbNTbHXRBSS+NccC0sSB4W7U67wID
# AQABo4IBSTCCAUUwHQYDVR0OBBYEFNk72sGDlH0r5DwvfGR5XwJI8B7bMB8GA1Ud
# IwQYMBaAFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMF8GA1UdHwRYMFYwVKBSoFCGTmh0
# dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY3Jvc29mdCUyMFRp
# bWUtU3RhbXAlMjBQQ0ElMjAyMDEwKDEpLmNybDBsBggrBgEFBQcBAQRgMF4wXAYI
# KwYBBQUHMAKGUGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY2VydHMv
# TWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUyMDIwMTAoMSkuY3J0MAwGA1Ud
# EwEB/wQCMAAwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwDgYDVR0PAQH/BAQDAgeA
# MA0GCSqGSIb3DQEBCwUAA4ICAQBlbu3IoynnPz0K1iPbeNnsej2b15l5sdl2FAFB
# BGT9lRdc2gNV8LAIusPYHHhUvRDcsx4lbMNhVKPGu4TDLaqNt/CI+SFtGuqdRLpV
# P1XE9cCLyKrKPpcJFJCqPpV+efoAtYBmIUQcxxwT7WIQ7gag8+rkKvrMkCoRqKS0
# mKv8J1sKfi85+G2uhZ/1RteSVdYZOZOj+Sb4wzonTCTj7EtgMN/BX35W5dTzd7wJ
# dGepYkVi871dSrC2Tr1ZFzAR7S44drCWZpJ6phJabVNOsNxFJKgSykugOGWzQ318
# Rr3MTPg2s3Bns+pUPVgMijd4bUOH2BlEsLMMwOcolTTZqg1HYrdY1jxpUAI9ipjB
# QRINL/O705Z+/f2LjNmJQooCVJVX24adpZ519SsfazGoqXGt91bmqKo0fI09Il4s
# UHh4ih6rpiQDBlyL7vmvCejwVxYevY4qVwTZ/o3gvl+R0lFxYS9feIM4NeG0+WsD
# Z7jLci5MFeuNwosQY3z26Xg1oj0U9u+ncR9uTU+xBmJ8BtlCdhQ13RNMX5P+krRY
# PB3XCp9Jm6XaO1995q32AIZm1mzBGI6yHlviXaEC5TzGiO1LXuPtXZU2X93oQJbM
# oe3v8+5CPKrQalGWyYuh2a3V1pwbj+W0FEmEFPpu8TI+qYO1IIQWUSRvFjXth5Ob
# 02hMMjCCB3EwggVZoAMCAQICEzMAAAAVxedrngKbSZkAAAAAABUwDQYJKoZIhvcN
# AQELBQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYD
# VQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xMjAw
# BgNVBAMTKU1pY3Jvc29mdCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAyMDEw
# MB4XDTIxMDkzMDE4MjIyNVoXDTMwMDkzMDE4MzIyNVowfDELMAkGA1UEBhMCVVMx
# EzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoT
# FU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUt
# U3RhbXAgUENBIDIwMTAwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDk
# 4aZM57RyIQt5osvXJHm9DtWC0/3unAcH0qlsTnXIyjVX9gF/bErg4r25PhdgM/9c
# T8dm95VTcVrifkpa/rg2Z4VGIwy1jRPPdzLAEBjoYH1qUoNEt6aORmsHFPPFdvWG
# UNzBRMhxXFExN6AKOG6N7dcP2CZTfDlhAnrEqv1yaa8dq6z2Nr41JmTamDu6Gnsz
# rYBbfowQHJ1S/rboYiXcag/PXfT+jlPP1uyFVk3v3byNpOORj7I5LFGc6XBpDco2
# LXCOMcg1KL3jtIckw+DJj361VI/c+gVVmG1oO5pGve2krnopN6zL64NF50ZuyjLV
# wIYwXE8s4mKyzbnijYjklqwBSru+cakXW2dg3viSkR4dPf0gz3N9QZpGdc3EXzTd
# EonW/aUgfX782Z5F37ZyL9t9X4C626p+Nuw2TPYrbqgSUei/BQOj0XOmTTd0lBw0
# gg/wEPK3Rxjtp+iZfD9M269ewvPV2HM9Q07BMzlMjgK8QmguEOqEUUbi0b1qGFph
# AXPKZ6Je1yh2AuIzGHLXpyDwwvoSCtdjbwzJNmSLW6CmgyFdXzB0kZSU2LlQ+QuJ
# YfM2BjUYhEfb3BvR/bLUHMVr9lxSUV0S2yW6r1AFemzFER1y7435UsSFF5PAPBXb
# GjfHCBUYP3irRbb1Hode2o+eFnJpxq57t7c+auIurQIDAQABo4IB3TCCAdkwEgYJ
# KwYBBAGCNxUBBAUCAwEAATAjBgkrBgEEAYI3FQIEFgQUKqdS/mTEmr6CkTxGNSnP
# EP8vBO4wHQYDVR0OBBYEFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMFwGA1UdIARVMFMw
# UQYMKwYBBAGCN0yDfQEBMEEwPwYIKwYBBQUHAgEWM2h0dHA6Ly93d3cubWljcm9z
# b2Z0LmNvbS9wa2lvcHMvRG9jcy9SZXBvc2l0b3J5Lmh0bTATBgNVHSUEDDAKBggr
# BgEFBQcDCDAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMCAYYw
# DwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTV9lbLj+iiXGJo0T2UkFvXzpoY
# xDBWBgNVHR8ETzBNMEugSaBHhkVodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtp
# L2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcmwwWgYIKwYB
# BQUHAQEETjBMMEoGCCsGAQUFBzAChj5odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20v
# cGtpL2NlcnRzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNydDANBgkqhkiG9w0B
# AQsFAAOCAgEAnVV9/Cqt4SwfZwExJFvhnnJL/Klv6lwUtj5OR2R4sQaTlz0xM7U5
# 18JxNj/aZGx80HU5bbsPMeTCj/ts0aGUGCLu6WZnOlNN3Zi6th542DYunKmCVgAD
# sAW+iehp4LoJ7nvfam++Kctu2D9IdQHZGN5tggz1bSNU5HhTdSRXud2f8449xvNo
# 32X2pFaq95W2KFUn0CS9QKC/GbYSEhFdPSfgQJY4rPf5KYnDvBewVIVCs/wMnosZ
# iefwC2qBwoEZQhlSdYo2wh3DYXMuLGt7bj8sCXgU6ZGyqVvfSaN0DLzskYDSPeZK
# PmY7T7uG+jIa2Zb0j/aRAfbOxnT99kxybxCrdTDFNLB62FD+CljdQDzHVG2dY3RI
# LLFORy3BFARxv2T5JL5zbcqOCb2zAVdJVGTZc9d/HltEAY5aGZFrDZ+kKNxnGSgk
# ujhLmm77IVRrakURR6nxt67I6IleT53S0Ex2tVdUCbFpAUR+fKFhbHP+CrvsQWY9
# af3LwUFJfn6Tvsv4O+S3Fb+0zj6lMVGEvL8CwYKiexcdFYmNcP7ntdAoGokLjzba
# ukz5m/8K6TT4JDVnK+ANuOaMmdbhIurwJ0I9JZTmdHRbatGePu1+oDEzfbzL6Xu/
# OHBE0ZDxyKs6ijoIYn/ZcGNTTY3ugm2lBRDBcQZqELQdVTNYs6FwZvKhggNQMIIC
# OAIBATCB+aGB0aSBzjCByzELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0
# b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3Jh
# dGlvbjElMCMGA1UECxMcTWljcm9zb2Z0IEFtZXJpY2EgT3BlcmF0aW9uczEnMCUG
# A1UECxMeblNoaWVsZCBUU1MgRVNOOjM3MDMtMDVFMC1EOTQ3MSUwIwYDVQQDExxN
# aWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNloiMKAQEwBwYFKw4DAhoDFQBLIMg1
# P7sNuCXpmbH2IXT2tXeEEKCBgzCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQI
# EwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3Nv
# ZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBD
# QSAyMDEwMA0GCSqGSIb3DQEBCwUAAgUA7jo56jAiGA8yMDI2MDgyNzA0MjIwMloY
# DzIwMjYwODI4MDQyMjAyWjB3MD0GCisGAQQBhFkKBAExLzAtMAoCBQDuOjnqAgEA
# MAoCAQACAjOrAgH/MAcCAQACAhSfMAoCBQDuO4tqAgEAMDYGCisGAQQBhFkKBAIx
# KDAmMAwGCisGAQQBhFkKAwKgCjAIAgEAAgMHoSChCjAIAgEAAgMBhqAwDQYJKoZI
# hvcNAQELBQADggEBAB00RzkO+P+57xcJkjGCvE9D15mNQMcVTTKj1ylsLyaxYcb9
# UccgTAIQu1ZIfKzpJSMsGAePGodLNgK6WGVoms1495TFZgHWaZaGJ2MAVOVhpN7T
# O9Bbt+mn5XFIvnOQTnWbkSU1rSl7NG0JgcSblB6ZNc2hw7ueLIKQGvP/flsuYG1Z
# 9+aSL0k7en7JAKcvkNy8tk93F8aByJ4Li4gsxB0e2kTDw65anlbn5R+sda7GQAsY
# 62St0AnqkBGtu8pKlx/kfDer14pU7yJiuXS+9lIxOjuVKvvP5aDCvTCYggPT5Pb7
# s2XMg8j0+mL9OefJmIQztZ7Gn+ScUQX+PztkCTcxggQNMIIECQIBATCBkzB8MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNy
# b3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMAITMwAAAh86cGnkojAulQABAAACHzAN
# BglghkgBZQMEAgEFAKCCAUowGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMC8G
# CSqGSIb3DQEJBDEiBCALENLereMuBeEp7oVI9Y2Gbs4VdDM1GmlRa7FjBpTj4DCB
# +gYLKoZIhvcNAQkQAi8xgeowgecwgeQwgb0EILAkCt9WkCsMtURkFu6TY0P3UXdR
# nCiYuPZhe3ykLfwUMIGYMIGApH4wfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldh
# c2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBD
# b3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIw
# MTACEzMAAAIfOnBp5KIwLpUAAQAAAh8wIgQgAY+/gKrLye+POXQFgTjJgzvXzW1y
# 0wfEo61cJkamdC8wDQYJKoZIhvcNAQELBQAEggIAyqSOPx87RtzwBCe4h/yCk6dP
# 6/YsD/PCeujGYgLyNt96wYYN5mTZCdmN/+n1iD3A/MMRSU61rsVqUgiv62WThCoS
# si2LCVS4JIS96tpipKOnPYPsZNS0QuHa7E6WWuKLo2tvCGfX7rz7f+fPfGvnVVnN
# /l+Bu1b2kGxc/o5sNk2LClX9lk4DCOEEGoJ43j+v2l44C2MK8UzOLdzhjOn2AczV
# lvrbqkOJ47eHIt0ek5vbsSSYOMw5KthCO8YGvvc7qj8HhyCT/tOloEplkFz4xO8L
# CvgNr8GslQNf2osps9WI8cjq5/Gopw6iqa6aBBgg8zq/89NjgxKbJKWP9p/MGfBE
# RjzbyWfcZ4jJomK/823ks8MrwzkQWzldtPpHQghFaxArfp32DeG471JMjtw2ql9S
# upyF34y2DaFfpqISsEvuaYK7JRybdtKDOfGYtkeaB0d/9pwKQYFrhm/NW27d1+xQ
# k1w88bjzKeOi3PuPsxnl83HX8MPFXHwN9idGrV0JY753PWGK5k3dj2WXD/33AT6j
# h1j6JtQs04ACSkHZTmVwSdSs7DBnZCnk9QJk7djkjua1nWnjNgkLMwOWEuASQAZw
# 67UnB7/mTQH2IHi7Ao6xgRYZd/huiMioW7JRUnb8kHeDl1Y47MEzuVBlRgIAjsst
# nmPW0mq4OEmKJizEQJY=
# SIG # End signature block