Functions/Private/Get-MigrationRuleImpact.ps1

function Get-MigrationRuleImpact {
    <#
    .SYNOPSIS
        Evaluates whether a proposed live migration would break, fix, or be neutral
        with respect to the configured affinity and anti-affinity rules.

    .DESCRIPTION
        For each rule that references the VM being migrated, the function simulates
        the post-migration placement and compares it to the current placement:

          Break (currently satisfied → will be violated):
            Hard rule → HasHardViolation = $true (caller should exclude this destination)
            Soft rule → HasSoftViolation = $true (caller should apply a score penalty)

          Fix (currently violated → will be satisfied):
            → FixesViolation = $true (caller should apply a score bonus)

          Neutral (no change in satisfaction status) → no flags set.

    .PARAMETER Placement
        Optional VMName → HostNode hashtable to evaluate against instead of the
        snapshot's own placement. Planners pass their simulated placement here so
        that migrations already planned earlier in the same pass are taken into
        account — otherwise two moves that are each rule-safe against the original
        placement could together break a hard rule.

    .OUTPUTS
        PSCustomObject: HasHardViolation, HasSoftViolation, FixesViolation,
                        HardReasons[], SoftReasons[], FixReasons[]
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [string]          $VMName,
        [Parameter(Mandatory)] [string]          $DestinationNode,
        [Parameter(Mandatory)] [PSCustomObject]  $Snapshot,
        [PSCustomObject[]]                        $RuleSet,
        [hashtable]                               $Placement
    )

    $empty = [PSCustomObject]@{
        HasHardViolation = $false
        HasSoftViolation = $false
        FixesViolation   = $false
        HardReasons      = @()
        SoftReasons      = @()
        FixReasons       = @()
    }

    if (-not $RuleSet -or $RuleSet.Count -eq 0) { return $empty }

    # Current placement: VMName → HostNode (simulated placement when supplied)
    $vmHost = @{}
    if ($null -ne $Placement) {
        foreach ($key in $Placement.Keys) { $vmHost[$key] = $Placement[$key] }
    } else {
        foreach ($vm in $Snapshot.VMs) { $vmHost[$vm.VMName] = $vm.HostNode }
    }

    $hardReasons = [System.Collections.Generic.List[string]]::new()
    $softReasons = [System.Collections.Generic.List[string]]::new()
    $fixReasons  = [System.Collections.Generic.List[string]]::new()

    foreach ($rule in $RuleSet) {
        if ($rule.VMs -notcontains $VMName) { continue }

        $activeVMs = @($rule.VMs | Where-Object { $vmHost.ContainsKey($_) })
        if ($activeVMs.Count -eq 0) { continue }

        $severity = if ($rule.Enforced) { 'enforced' } else { 'soft' }

        switch ($rule.Type) {

            'VmVmAffinity' {
                # All members must share one host.
                $currentHosts = @($activeVMs | ForEach-Object { $vmHost[$_] } | Select-Object -Unique)
                $simHosts     = @($activeVMs | ForEach-Object {
                    if ($_ -eq $VMName) { $DestinationNode } else { $vmHost[$_] }
                } | Select-Object -Unique)

                $wasSatisfied  = $currentHosts.Count -le 1
                $willSatisfied = $simHosts.Count     -le 1

                if ($wasSatisfied -and -not $willSatisfied) {
                    $msg = "Breaks $severity affinity rule '$($rule.Name)' — members would span: $($simHosts -join ', ')"
                    if ($rule.Enforced) { $hardReasons.Add($msg) } else { $softReasons.Add($msg) }
                } elseif (-not $wasSatisfied -and $willSatisfied) {
                    $fixReasons.Add("Satisfies affinity rule '$($rule.Name)' — all members consolidated on '$DestinationNode'")
                }
            }

            'VmVmAntiAffinity' {
                # No two members may share a host.
                $currentConflicts = @($activeVMs | Group-Object { $vmHost[$_]  } | Where-Object { $_.Count -gt 1 })
                $simConflicts     = @($activeVMs | Group-Object {
                    if ($_ -eq $VMName) { $DestinationNode } else { $vmHost[$_] }
                } | Where-Object { $_.Count -gt 1 })

                $wasSatisfied  = $currentConflicts.Count -eq 0
                $willSatisfied = $simConflicts.Count     -eq 0

                if ($wasSatisfied -and -not $willSatisfied) {
                    $peer = ($simConflicts[0].Group | Where-Object { $_ -ne $VMName })[0]
                    $msg  = "Breaks $severity anti-affinity rule '$($rule.Name)' — '$VMName' would share '$DestinationNode' with '$peer'"
                    if ($rule.Enforced) { $hardReasons.Add($msg) } else { $softReasons.Add($msg) }
                } elseif (-not $wasSatisfied -and $willSatisfied) {
                    $fixReasons.Add("Satisfies anti-affinity rule '$($rule.Name)' — '$VMName' separated from all group members")
                }
            }

            'VmHostAffinity' {
                # VM must reside on one of the listed hosts.
                $wasSatisfied  = $rule.Hosts -contains $vmHost[$VMName]
                $willSatisfied = $rule.Hosts -contains $DestinationNode

                if ($wasSatisfied -and -not $willSatisfied) {
                    $msg = "Breaks $severity host-affinity rule '$($rule.Name)' — '$DestinationNode' is not in [$(($rule.Hosts) -join ', ')]"
                    if ($rule.Enforced) { $hardReasons.Add($msg) } else { $softReasons.Add($msg) }
                } elseif (-not $wasSatisfied -and $willSatisfied) {
                    $fixReasons.Add("Satisfies host-affinity rule '$($rule.Name)' — '$VMName' moves onto allowed host '$DestinationNode'")
                }
            }

            'VmHostAntiAffinity' {
                # VM must NOT reside on any of the listed hosts.
                $wasSatisfied  = $rule.Hosts -notcontains $vmHost[$VMName]
                $willSatisfied = $rule.Hosts -notcontains $DestinationNode

                if ($wasSatisfied -and -not $willSatisfied) {
                    $msg = "Breaks $severity host-anti-affinity rule '$($rule.Name)' — '$DestinationNode' is an excluded host"
                    if ($rule.Enforced) { $hardReasons.Add($msg) } else { $softReasons.Add($msg) }
                } elseif (-not $wasSatisfied -and $willSatisfied) {
                    $fixReasons.Add("Satisfies host-anti-affinity rule '$($rule.Name)' — '$VMName' moves off excluded host")
                }
            }
        }
    }

    [PSCustomObject]@{
        HasHardViolation = $hardReasons.Count -gt 0
        HasSoftViolation = $softReasons.Count -gt 0
        FixesViolation   = $fixReasons.Count  -gt 0
        HardReasons      = $hardReasons.ToArray()
        SoftReasons      = $softReasons.ToArray()
        FixReasons       = $fixReasons.ToArray()
    }
}