Write a reviewable network change report
Export proposed access-VLAN changes separately from missing observations, with an honest coverage summary.
A proposal needs a reason and a known starting point
The supplied assessments describe expected access ports as Compliant, Drift, or Unknown. For Drift, propose SetAccessVlan with the observed CurrentVlan and DesiredVlan. For Unknown, propose Investigate: there is no observed starting configuration to support a direct change proposal.
Compliant ports produce no proposal, but they still contribute to assessment coverage. Return a CSV with DeviceId, Port, Action, CurrentVlan, and DesiredVlan. These are structured review records, not executable vendor commands.
Report coverage alongside changes
summary.json must retain PortCount, ChangeCount, UnknownCount, Coverage, and a sorted unique Devices array. Coverage is Empty when there were no expected ports, Incomplete when any observation is Unknown, and Complete otherwise. Complete does not mean zero drift; it means the required observations were available.
Get-Content '/home/user/assessments.json' -Raw | ConvertFrom-Json | Group-Object State
Keep empty CSV headers and preserve Devices as an array for zero or one device. Repeated runs replace the saved reports so stale proposals do not accumulate. Re-import the actual output files to check their structure.
This workflow changes only the virtual report files. Device access, credentials, approvals, vendor command generation, application, and rollback belong to a separately designed operational workflow.
Get-Content '/home/user/assessments.json' -Raw | ConvertFrom-Json
Put it into practice.
Define Write-NetworkChangeReport -Assessments -OutputDirectory. Write proposals.csv and summary.json with the contracts above. Drift proposes SetAccessVlan; Unknown proposes Investigate; Compliant generates no proposal. Preserve array/header shapes, create the destination, and replace outputs on repeated calls. Return ProposalCount and Coverage.
See an explained solution
function Write-NetworkChangeReport {
param([AllowEmptyCollection()][object[]]$Assessments,[string]$OutputDirectory)
$rows = @($Assessments)
$proposals = @(foreach ($row in $rows) {
if ($row.State -eq 'Compliant') { continue }
[pscustomobject]@{ DeviceId=$row.DeviceId; Port=$row.Port; Action=$(if ($row.State -eq 'Unknown') { 'Investigate' } else { 'SetAccessVlan' }); CurrentVlan=$row.CurrentVlan; DesiredVlan=$row.DesiredVlan }
})
$unknown = @($rows | Where-Object State -EQ Unknown).Count
$changes = @($rows | Where-Object State -EQ Drift).Count
$coverage = if ($rows.Count -eq 0) { 'Empty' } elseif ($unknown -gt 0) { 'Incomplete' } else { 'Complete' }
$summary = [pscustomobject]@{ PortCount=$rows.Count; ChangeCount=$changes; UnknownCount=$unknown; Coverage=$coverage; Devices=@($rows.DeviceId | Sort-Object -Unique) }
New-Item -ItemType Directory -Path $OutputDirectory -Force | Out-Null
$path = Join-Path $OutputDirectory 'proposals.csv'
if ($proposals.Count) { $proposals | Export-Csv -LiteralPath $path -NoTypeInformation }
else { 'DeviceId,Port,Action,CurrentVlan,DesiredVlan' | Set-Content -LiteralPath $path }
$summary | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath (Join-Path $OutputDirectory 'summary.json')
[pscustomobject]@{ ProposalCount=$proposals.Count; Coverage=$coverage }
}
Only observed drift becomes a proposed configuration change. Missing observations become investigation work, while the summary keeps coverage separate from the number of changes.