Private/Model/Export-PpaSnapshot.ps1

# Export-PpaSnapshot.ps1 - serializes and writes the snapshot model (Wave 4 spec 3.1).
# Serialization rule (spec section 1, pinned): ConvertTo-Json -InputObject ... -Depth 16
# ALWAYS - PS 5.1 truncates silently past -Depth, and the torture fixture's depth
# canary proves 16 is sufficient. Snapshots are ALWAYS UNREDACTED in v1 (ruled): they
# carry UPNs and scope identities and are engagement-confidential; the redacted HTML
# report is the artifact that travels. A one-line console notice says so on every write.
# ASCII-only source (Windows PowerShell 5.1).

Set-StrictMode -Off

function Get-PpaTenantIdShort {
    # Filename token: lowercase alphanumerics of tenantId, first 8 chars; 'unknown'
    # when the snapshot has no tenant identity.
    param([string]$TenantId)
    $s = ([string]$TenantId).ToLowerInvariant() -replace '[^a-z0-9]', ''
    if ($s.Length -eq 0) { return 'unknown' }
    if ($s.Length -gt 8) { return $s.Substring(0, 8) }
    return $s
}

function Export-PpaSnapshot {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)] $Model,
        [Parameter(Mandatory = $true)][string]$Directory,
        # Raw collector outputs (section id -> output); only written with -IncludeRawCapture.
        $RawMap = $null,
        [switch]$IncludeRawCapture
    )

    if (-not (Test-Path -LiteralPath $Directory)) {
        New-Item -ItemType Directory -Path $Directory -Force | Out-Null
    }

    # PPA-Snapshot_<tenantIdShort>_<capturedAtCompact>Z_<snapshotId8>.json
    # capturedAt is already ISO-8601 UTC ('2026-07-03T14:15:00Z'); stripping '-' and
    # ':' yields the compact form including the trailing Z. snapshotId8 = first 8
    # chars of the GUID string (opaque - no parsing).
    $short   = Get-PpaTenantIdShort ([string]$Model.tenantId)
    $compact = ([string]$Model.capturedAt) -replace '-', '' -replace ':', ''
    $id8     = ([string]$Model.snapshotId).Substring(0, [Math]::Min(8, ([string]$Model.snapshotId).Length))
    $suffix  = '{0}_{1}_{2}.json' -f $short, $compact, $id8

    $snapshotPath = Join-Path $Directory ('PPA-Snapshot_' + $suffix)
    $json = ConvertTo-Json -InputObject $Model -Depth 16
    [System.IO.File]::WriteAllText($snapshotPath, $json, (New-Object System.Text.UTF8Encoding($false)))
    Write-Host ("Snapshot : {0} (contains unredacted UPNs and scope identities; treat as engagement-confidential - the redacted HTML report is the artifact that travels)" -f $snapshotPath)

    $rawPath = $null
    if ($IncludeRawCapture) {
        # Debug artifact OUTSIDE the schema - never referenced by delta (ruled 3.1).
        $rawPath = Join-Path $Directory ('PPA-RawCapture_' + $suffix)
        $rawOrdered = [ordered]@{}
        foreach ($k in @($RawMap.Keys | Sort-Object)) { $rawOrdered[[string]$k] = $RawMap[$k] }
        [System.IO.File]::WriteAllText($rawPath, (ConvertTo-Json -InputObject $rawOrdered -Depth 16), (New-Object System.Text.UTF8Encoding($false)))
        Write-Host ("RawCapture: {0} (debug only - outside the snapshot schema)" -f $rawPath)
    }

    return [pscustomobject]@{ SnapshotPath = $snapshotPath; RawCapturePath = $rawPath }
}