Netscoot.Core/Private/SemVer.ps1

function Compare-NetscootSemVer {
    <#
    Compare two SemVer 2.0 version strings (optionally `v`-prefixed, with an optional prerelease tag).
    Returns -1 when Reference < Difference, 0 when equal, 1 when Reference > Difference.
 
    Core (major.minor.patch) compares numerically first. On equal cores, a version WITHOUT a
    prerelease outranks one WITH (stable > prerelease). When both have a prerelease, the dot-separated
    identifiers compare left-to-right: all-digit identifiers numerically; an all-digit identifier ranks
    below an alphanumeric one; alphanumerics compare ordinally; if every shared identifier is equal,
    the longer identifier list wins. (Build metadata is not modeled - netscoot tags never carry it.)
    #>

    param(
        [Parameter(Mandatory)][string]$Reference,
        [Parameter(Mandatory)][string]$Difference
    )

    $parse = {
        param($s)
        if ("$s" -notmatch '^v?(\d+)\.(\d+)\.(\d+)(?:-(.+))?$') {
            throw "Not a parseable version: '$s'"
        }
        [pscustomobject]@{
            Core = [int[]]@([int]$Matches[1], [int]$Matches[2], [int]$Matches[3])
            Pre  = if ($Matches[4]) { @($Matches[4].Split('.')) } else { @() }
        }
    }

    $a = & $parse $Reference
    $b = & $parse $Difference

    for ($i = 0; $i -lt 3; $i++) {
        if ($a.Core[$i] -lt $b.Core[$i]) { return -1 }
        if ($a.Core[$i] -gt $b.Core[$i]) { return 1 }
    }

    # Equal cores. A stable (no prerelease) outranks a prerelease.
    $aHasPre = (@($a.Pre).Count -gt 0)
    $bHasPre = (@($b.Pre).Count -gt 0)
    if (-not $aHasPre -and -not $bHasPre) { return 0 }
    if (-not $aHasPre) { return 1 }
    if (-not $bHasPre) { return -1 }

    # Both prerelease: compare identifiers left-to-right per SemVer 2.0 precedence. Re-wrap with @()
    # so a single-identifier prerelease stays an array (a scalar string would index per-character).
    $aPre = @($a.Pre)
    $bPre = @($b.Pre)
    $n = [Math]::Min($aPre.Count, $bPre.Count)
    for ($i = 0; $i -lt $n; $i++) {
        $ai = $aPre[$i]; $bi = $bPre[$i]
        $aNum = ($ai -match '^\d+$')
        $bNum = ($bi -match '^\d+$')
        if ($aNum -and $bNum) {
            $av = [int]$ai; $bv = [int]$bi
            if ($av -lt $bv) { return -1 }
            if ($av -gt $bv) { return 1 }
        } elseif ($aNum -ne $bNum) {
            # Numeric identifiers always have lower precedence than alphanumeric ones.
            return $(if ($aNum) { -1 } else { 1 })
        } else {
            $cmp = [string]::CompareOrdinal($ai, $bi)
            if ($cmp -lt 0) { return -1 }
            if ($cmp -gt 0) { return 1 }
        }
    }

    # All shared identifiers equal: the longer list wins.
    if ($aPre.Count -lt $bPre.Count) { return -1 }
    if ($aPre.Count -gt $bPre.Count) { return 1 }
    return 0
}