Private/ConvertTo-TLNormalizedObject.ps1

function ConvertTo-TLNormalizedObject {
    <#
    .SYNOPSIS
        Normalizes a data tree for deterministic diffing.
    .DESCRIPTION
        - removes volatile properties (case-insensitive)
        - orders object properties alphabetically
        - sorts arrays deterministically (by id/domain/skuPartNumber for object
          arrays, by value for scalar arrays)
    #>

    [CmdletBinding()]
    param(
        [AllowNull()]
        [object]$InputObject,

        [string[]]$VolatileProperty = @()
    )

    if ($null -eq $InputObject) { return $null }
    if ($InputObject -is [string] -or $InputObject.GetType().IsValueType) { return $InputObject }

    if ($InputObject -is [System.Collections.IDictionary]) {
        $normalized = [ordered]@{}
        foreach ($key in (@($InputObject.Keys) | Sort-Object)) {
            if ($VolatileProperty -contains [string]$key) { continue }
            $normalized[[string]$key] = ConvertTo-TLNormalizedObject -InputObject $InputObject[$key] -VolatileProperty $VolatileProperty
        }
        return $normalized
    }

    if ($InputObject -is [System.Collections.IEnumerable]) {
        $items = @(foreach ($item in $InputObject) {
                ConvertTo-TLNormalizedObject -InputObject $item -VolatileProperty $VolatileProperty
            })
        if ($items.Count -gt 1) {
            $first = $items[0]
            if ($first -is [System.Collections.IDictionary]) {
                $sortKey = $null
                foreach ($candidate in @('id', 'domain', 'skuPartNumber', 'displayName')) {
                    if ($first.Contains($candidate)) { $sortKey = $candidate; break }
                }
                if ($sortKey) {
                    $items = @($items | Sort-Object -Property @{ Expression = { [string]$_[$sortKey] } })
                }
            }
            else {
                $items = @($items | Sort-Object -Property @{ Expression = { [string]$_ } })
            }
        }
        return , $items
    }

    # PSCustomObject
    $normalized = [ordered]@{}
    foreach ($property in ($InputObject.PSObject.Properties | Sort-Object -Property Name)) {
        if ($VolatileProperty -contains $property.Name) { continue }
        $normalized[$property.Name] = ConvertTo-TLNormalizedObject -InputObject $property.Value -VolatileProperty $VolatileProperty
    }
    return $normalized
}