Private/Scoring.ps1

# Scoring — the single source of the compliance-score policy.
#
# The rule (PASS / (PASS+FAIL+ERROR); ERROR counts as failing; INFO/MANUAL/SUPPRESSED
# excluded) is enforced here and nowhere else. Console summary, HTML report, run
# history, and the exit code all call these functions, so the score can never drift
# between outputs.

function Get-AuditCounts {
    <#
    .SYNOPSIS
    Count results per status. Returns a hashtable keyed PASS/FAIL/ERROR/INFO/MANUAL/
    SUPPRESSED; unknown statuses are ignored.
    #>

    param([AllowEmptyCollection()][object[]]$Results = @())

    $counts = @{ PASS = 0; FAIL = 0; ERROR = 0; INFO = 0; MANUAL = 0; SUPPRESSED = 0 }
    foreach ($r in $Results) {
        if ($counts.ContainsKey($r.Status)) { $counts[$r.Status]++ }
    }
    return $counts
}

function Get-AssessedCount {
    <#
    .SYNOPSIS
    Number of scored controls: PASS + FAIL + ERROR (INFO/MANUAL/SUPPRESSED excluded).
    Accepts any hashtable with those keys, so per-level breakdowns work too.
    #>

    param([Parameter(Mandatory)][hashtable]$Counts)

    return [int]$Counts.PASS + [int]$Counts.FAIL + [int]$Counts.ERROR
}

function Get-AuditScore {
    <#
    .SYNOPSIS
    Compliance score as a percentage rounded to one decimal: 100 * PASS / assessed.
    Returns 0 when nothing was assessed.
    #>

    param([Parameter(Mandatory)][hashtable]$Counts)

    $assessed = Get-AssessedCount -Counts $Counts
    if ($assessed -eq 0) { return 0 }
    return [math]::Round(100.0 * $Counts.PASS / $assessed, 1)
}