param( [Parameter(Mandatory = $true)][string]$AuditPath, [Parameter(Mandatory = $true)][string]$IdempotencyPath ) Set-StrictMode -Version 2.0 $ErrorActionPreference = "Stop" function Read-JsonLines { param([Parameter(Mandatory = $true)][string]$Path) if ([string]::IsNullOrWhiteSpace($Path) -or -not (Test-Path -LiteralPath $Path)) { return @() } $records = @() $lineNo = 0 foreach ($line in Get-Content -LiteralPath $Path) { $lineNo++ $trimmed = $line.Trim() if ($trimmed.Length -eq 0) { continue } try { $records += ($trimmed | ConvertFrom-Json) } catch { throw "failed to parse JSONL $Path line ${lineNo}: $($_.Exception.Message)" } } return @($records) } function Get-JsonPropertyNames { param([AllowNull()]$Value) $names = @() if ($null -eq $Value) { return $names } if ($Value -is [System.Array]) { foreach ($item in $Value) { $names += Get-JsonPropertyNames -Value $item } return $names } if ($Value -is [System.Collections.IDictionary]) { foreach ($key in $Value.Keys) { $names += [string]$key $names += Get-JsonPropertyNames -Value $Value[$key] } return $names } if ($Value.PSObject -and $Value.PSObject.Properties) { foreach ($property in $Value.PSObject.Properties) { $names += $property.Name $names += Get-JsonPropertyNames -Value $property.Value } } return $names } function Get-StringProperty { param( [Parameter(Mandatory = $true)]$Record, [Parameter(Mandatory = $true)][string]$Name ) $property = $Record.PSObject.Properties[$Name] if ($null -eq $property -or $null -eq $property.Value) { return "" } return [string]$property.Value } $auditRecords = @(Read-JsonLines -Path $AuditPath) $idempotencyRecords = @(Read-JsonLines -Path $IdempotencyPath) $allRecords = @($auditRecords + $idempotencyRecords) $propertyNames = @() foreach ($record in $allRecords) { $propertyNames += Get-JsonPropertyNames -Value $record } $rawBodyKeys = @("content_text", "ContentText", "message_body", "raw_body", "raw_content_text") $rawIdempotencyKeys = @("idempotency_key", "IdempotencyKey", "raw_idempotency_key") $rawBodyPresent = $false $rawIdempotencyKeyPresent = $false foreach ($name in $propertyNames) { foreach ($rawName in $rawBodyKeys) { if ([string]::Equals($name, $rawName, [System.StringComparison]::OrdinalIgnoreCase)) { $rawBodyPresent = $true } } foreach ($rawName in $rawIdempotencyKeys) { if ([string]::Equals($name, $rawName, [System.StringComparison]::OrdinalIgnoreCase)) { $rawIdempotencyKeyPresent = $true } } } $byIdempotencyKey = @{} foreach ($record in $auditRecords) { $keyHash = Get-StringProperty -Record $record -Name "idempotency_key_sha256" if ([string]::IsNullOrWhiteSpace($keyHash)) { continue } $fingerprint = (Get-StringProperty -Record $record -Name "target_ref") + "|" + (Get-StringProperty -Record $record -Name "content_sha256") if (-not $byIdempotencyKey.ContainsKey($keyHash)) { $byIdempotencyKey[$keyHash] = @() } $byIdempotencyKey[$keyHash] = @($byIdempotencyKey[$keyHash] + $fingerprint) } $duplicateDetected = $false $conflictDetected = $false foreach ($keyHash in $byIdempotencyKey.Keys) { $fingerprints = @($byIdempotencyKey[$keyHash]) if ($fingerprints.Count -lt 2) { continue } $unique = @($fingerprints | Sort-Object -Unique) if ($unique.Count -eq 1) { $duplicateDetected = $true } else { $conflictDetected = $true $counts = @{} foreach ($fingerprint in $fingerprints) { if (-not $counts.ContainsKey($fingerprint)) { $counts[$fingerprint] = 0 } $counts[$fingerprint]++ } foreach ($count in $counts.Values) { if ($count -gt 1) { $duplicateDetected = $true } } } } $ok = -not ($rawBodyPresent -or $rawIdempotencyKeyPresent) $summary = [ordered]@{ ok = $ok verification = "send_audit_idempotency" audit_records = $auditRecords.Count idempotency_records = $idempotencyRecords.Count duplicate_detected = $duplicateDetected conflict_detected = $conflictDetected raw_body_present = $rawBodyPresent raw_idempotency_key_present = $rawIdempotencyKeyPresent audit_path_configured = -not [string]::IsNullOrWhiteSpace($AuditPath) idempotency_path_configured = -not [string]::IsNullOrWhiteSpace($IdempotencyPath) } Write-Output ($summary | ConvertTo-Json -Compress) if (-not $ok) { exit 1 }