2026-08-20 21:48:19 -04:00
|
|
|
function Write-PersonaSummary {
|
|
|
|
|
<#
|
|
|
|
|
.SYNOPSIS
|
|
|
|
|
Renders the rule-match table, outcome totals, and reconciliation result
|
|
|
|
|
(FR-019, FR-020, FR-021).
|
|
|
|
|
|
|
|
|
|
.DESCRIPTION
|
|
|
|
|
Emitted every summaryInterval users and once at completion.
|
|
|
|
|
|
|
|
|
|
Every business rule appears, including disabled rules and rules with zero
|
|
|
|
|
matches. A rule that never fired and a rule that is not in the configuration
|
|
|
|
|
look identical if zero-match rules are omitted, and the difference is exactly
|
|
|
|
|
what an operator investigating "why did nobody get classified as Tier0" needs
|
|
|
|
|
to see.
|
|
|
|
|
|
|
|
|
|
Reconciliation is displayed on every summary, not only when it fails. A check
|
|
|
|
|
that is only visible when broken gives an operator no reason to believe it
|
|
|
|
|
ran at all.
|
|
|
|
|
|
|
|
|
|
.PARAMETER Counters
|
|
|
|
|
The run counter set.
|
|
|
|
|
|
|
|
|
|
.PARAMETER SummaryType
|
|
|
|
|
Interim or Final. Final is emitted regardless of interval, including when
|
|
|
|
|
the interval is 0 (FR-020).
|
|
|
|
|
|
|
|
|
|
.PARAMETER Mode
|
|
|
|
|
Preview or Enforce, shown in the header so a screenshot of a summary is
|
|
|
|
|
self-describing.
|
2026-08-24 22:10:24 -04:00
|
|
|
|
|
|
|
|
.PARAMETER StartedUtc
|
|
|
|
|
Run start timestamp. When supplied, the header shows elapsed wall-clock
|
|
|
|
|
time since the run began. Optional so callers that only care about counts
|
|
|
|
|
are not forced to thread a clock through.
|
2026-08-20 21:48:19 -04:00
|
|
|
#>
|
|
|
|
|
[CmdletBinding()]
|
|
|
|
|
param(
|
|
|
|
|
[Parameter(Mandatory)]
|
|
|
|
|
[object] $Counters,
|
|
|
|
|
|
|
|
|
|
[ValidateSet('Interim', 'Final')]
|
|
|
|
|
[string] $SummaryType = 'Interim',
|
|
|
|
|
|
|
|
|
|
[ValidateSet('Preview', 'Enforce')]
|
2026-08-24 22:10:24 -04:00
|
|
|
[string] $Mode = 'Preview',
|
|
|
|
|
|
|
|
|
|
[Nullable[datetime]] $StartedUtc
|
2026-08-20 21:48:19 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
$reconciled = Test-PersonaReconciliation -Counters $Counters
|
|
|
|
|
|
2026-08-24 22:10:24 -04:00
|
|
|
# $StartedUtc arrives here already unwrapped to a plain [datetime] - PowerShell
|
|
|
|
|
# collapses [Nullable[datetime]] to DateTime (or $null) at the call boundary, so
|
|
|
|
|
# a null check is used rather than .Value / .HasValue.
|
|
|
|
|
$elapsed = ($null -ne $StartedUtc) ? ('{0:hh\:mm\:ss}' -f ([DateTime]::UtcNow - $StartedUtc)) : $null
|
|
|
|
|
|
2026-08-20 21:48:19 -04:00
|
|
|
Write-Host ''
|
|
|
|
|
Write-Host ('=' * 100) -ForegroundColor DarkGray
|
2026-08-24 22:10:24 -04:00
|
|
|
Write-Host (
|
|
|
|
|
$elapsed `
|
|
|
|
|
? ("{0} summary - mode: {1} - elapsed: {2} - processed: {3}" -f $SummaryType, $Mode, $elapsed, $Counters.Processed) `
|
|
|
|
|
: ("{0} summary - mode: {1} - processed: {2}" -f $SummaryType, $Mode, $Counters.Processed)
|
|
|
|
|
) -ForegroundColor Cyan
|
2026-08-20 21:48:19 -04:00
|
|
|
Write-Host ('=' * 100) -ForegroundColor DarkGray
|
|
|
|
|
|
|
|
|
|
Write-Host ('{0,-28} {1,-40} {2,-9} {3,10} {4,8}' -f 'Rule ID', 'Name', 'Priority', 'Enabled', 'Matches') -ForegroundColor DarkGray
|
|
|
|
|
|
|
|
|
|
foreach ($entry in $Counters.RuleCounts) {
|
|
|
|
|
# A disabled rule is dimmed rather than hidden: it is part of the
|
|
|
|
|
# configuration and its absence from the output would read as a deletion.
|
|
|
|
|
$colour = if (-not $entry.Enabled) { 'DarkGray' } elseif ($entry.Matches -gt 0) { 'Green' } else { 'Gray' }
|
|
|
|
|
|
|
|
|
|
Write-Host ('{0,-28} {1,-40} {2,-9} {3,10} {4,8}' -f
|
|
|
|
|
$entry.RuleId,
|
|
|
|
|
($entry.Name.Length -gt 40 ? $entry.Name.Substring(0, 37) + '...' : $entry.Name),
|
|
|
|
|
$entry.Priority,
|
|
|
|
|
$entry.Enabled,
|
|
|
|
|
$entry.Matches) -ForegroundColor $colour
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Write-Host ''
|
|
|
|
|
Write-Host ('Outcomes Matched: {0} Unclassified: {1} EvaluationError: {2}' -f
|
|
|
|
|
$Counters.Matched, $Counters.Unclassified, $Counters.EvaluationError)
|
|
|
|
|
Write-Host ('Actions Unchanged: {0} WouldUpdate: {1} Updated: {2} UpdateFailed: {3} Skipped: {4}' -f
|
|
|
|
|
$Counters.Unchanged, $Counters.WouldUpdate, $Counters.Updated, $Counters.UpdateFailed, $Counters.Skipped)
|
|
|
|
|
|
|
|
|
|
if ($reconciled) {
|
|
|
|
|
Write-Host ('Reconciliation PASS {0} = {1} + {2} + {3}' -f
|
|
|
|
|
$Counters.Processed, $Counters.Matched, $Counters.Unclassified, $Counters.EvaluationError) -ForegroundColor Green
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
Write-Host ('Reconciliation FAIL {0} != {1} + {2} + {3} - this is an engine defect (FR-021)' -f
|
|
|
|
|
$Counters.Processed, $Counters.Matched, $Counters.Unclassified, $Counters.EvaluationError) -ForegroundColor Red
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Write-Host ('=' * 100) -ForegroundColor DarkGray
|
|
|
|
|
Write-Host ''
|
|
|
|
|
}
|