Private/Collect/PpaNormalize.ps1
|
# PpaNormalize.ps1 - the collector-side normalize contract (Wave 4 Part A). # Three rules every collector output obeys, pinned by Tests\Collect.Contract.Tests.ps1: # 1. Every leaf is string / number / boolean / null (PS 5.1 serializes enums as # integers and dates in invariant-culture format, so anything non-primitive is # stringified HERE, never at the snapshot writer). # 2. DateTime values become ISO-8601 UTC strings at normalize time. # 3. Session artifacts from remoting never survive into normalized objects. # Also home to the per-collector outcome enum resolver (spec A.4): # Populated | Empty | Partial | AccessDenied | CmdletUnavailable | Failed # | Skipped | NotRun # ASCII-only source (Windows PowerShell 5.1). Set-StrictMode -Off function ConvertTo-PpaIso8601 { # DateTime -> ISO-8601 UTC string ('yyyy-MM-ddTHH:mm:ssZ'). Strings pass through # untouched so fixture dates and live string-typed dates agree. Null and the # DateTime.MinValue placeholder (what EXO returns for "never") become '' so a # policy is never reported as "in test mode since 01-Jan-0001". # Kind-unspecified values are treated as local time, matching how PowerShell # remoting deserializes DateTimes. param($Value) if ($null -eq $Value) { return '' } if ($Value -is [datetime]) { if ($Value -eq [datetime]::MinValue) { return '' } return $Value.ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ', [System.Globalization.CultureInfo]::InvariantCulture) } return [string]$Value } function Get-PpaOptionalGuid { # A.5 opportunistic Guid capture: project the raw object's Guid when the cmdlet # provides one, so the snapshot keying rule (Guid -> Identity -> Name) and the # delta rename-reconciliation pass can operate. Property-presence check only - # no new reads, no new cmdlets. Provenance: documented-only (the Guid property # is documented on these cmdlets but not on the live-verified list). # Returns '' when the property is absent, null, or the all-zeros placeholder, # letting the keying rule fall back to Name. param($Object) if ($null -eq $Object) { return '' } if ($Object.PSObject.Properties.Name -notcontains 'Guid') { return '' } $g = [string]$Object.Guid if ([string]::IsNullOrEmpty($g) -or $g -eq '00000000-0000-0000-0000-000000000000') { return '' } return $g } function Get-PpaLocationScopeToken { # Part D matrix grounding: collapse a raw *Location property to the closed # scope token set - 'All' (contains the All token), 'Scoped' (specific # includes), 'None' (absent or empty). StrictMode is off, so passing a # property that does not exist on the raw object arrives as $null -> 'None'. param($Location) $vals = @($Location | Where-Object { $null -ne $_ }) if ($vals.Count -eq 0) { return 'None' } foreach ($v in $vals) { if ([string]$v -eq 'All') { return 'All' } } return 'Scoped' } function Test-PpaLocationException { # True when a raw *LocationException property carries any entry. param($LocationException) return (@($LocationException | Where-Object { $_ }).Count -gt 0) } function Get-PpaSessionArtifactNames { # Property names PowerShell remoting stamps on deserialized objects. Generic # projections (ones that copy every property) must skip these; explicit # projections never pick them up in the first place. return @('RunspaceId', 'PSComputerName', 'PSShowComputerName', 'PSSourceJobInstanceId', 'PSJobTypeName') } function Resolve-PpaCollectorOutcome { # Derive the per-collector outcome from the statuses of every read the collector # performed (Invoke-PpaReadCmdlet statuses: Ok | AccessDenied | CommandNotFound | # Error | Blocked) plus the number of normalized items produced. # all reads Ok -> Populated (items > 0) / Empty (zero items) # some Ok, some failed -> Partial (visibility degraded, data incomplete) # no read Ok -> the most actionable uniform cause, by precedence: # AccessDenied (fix roles) > CmdletUnavailable # (fix module/connection) > Failed (investigate) # Skipped / NotRun belong to the orchestration layer (a collector that actually # ran never reports them); they are stamped when a section is not selected. param( [Parameter(Mandatory = $true)][AllowEmptyCollection()][string[]]$ReadStatuses, [int]$ItemCount = 0 ) $statuses = @($ReadStatuses) if ($statuses.Count -eq 0) { return 'Failed' } $okCount = @($statuses | Where-Object { $_ -eq 'Ok' }).Count if ($okCount -eq $statuses.Count) { if ($ItemCount -gt 0) { return 'Populated' } return 'Empty' } if ($okCount -gt 0) { return 'Partial' } if ($statuses -contains 'AccessDenied') { return 'AccessDenied' } if ($statuses -contains 'CommandNotFound') { return 'CmdletUnavailable' } return 'Failed' } |