Public/Export-AzLocalClusterUpdateReadinessReport.ps1

function Export-AzLocalClusterUpdateReadinessReport {
    <#
    .SYNOPSIS
        Runs the Step.5 pre-flight Update Readiness Assessment workload:
        Get-AzLocalClusterUpdateReadiness + Test-AzLocalClusterHealth
        -BlockingOnly against a target UpdateRing (or whole fleet),
        writes per-check CSV/JUnit XML artifacts, merges them into a
        combined JUnit report, and emits the markdown step summary +
        step outputs for the v0.8.5 thin-YAML Step.5 pipeline.
 
    .DESCRIPTION
        Phase 1 (v0.8.5) of the thin-YAML refactor. Condenses the inline
        `run: |` body of the v0.8.4 Step.5_assess-update-readiness.yml
        (GitHub Actions + Azure DevOps) into a single cmdlet call so the
        per-platform yml shrinks to a few lines and the workload becomes
        unit-testable against synthetic Get-AzLocalClusterUpdateReadiness
        and Test-AzLocalClusterHealth results.
 
        The cmdlet:
 
          1. Resolves the output directory (defaults to './artifacts' on
             GitHub Actions / Local, or `$env:BUILD_ARTIFACTSTAGINGDIRECTORY`
             on Azure DevOps - matching the v0.8.4 yml).
          2. Calls `Get-AzLocalClusterInventory -PassThru` to build a
             ResourceId -> UpdateRing map for the per-ring pivot section.
          3. When -Scope is 'all' and the inventory is empty, short-
             circuits with zero counts, an IDLE markdown summary, and
             empty step outputs (matches the v0.8.4 yml early-exit).
          4. Calls `Get-AzLocalClusterUpdateReadiness` TWICE so the
             cmdlet's native -ExportPath emitter produces both the
             readiness.csv (humans) and readiness.xml (JUnit, one
             <testcase> per cluster). This preserves the v0.8.4
             dorny/test-reporter contract byte-for-byte.
          5. Calls `Test-AzLocalClusterHealth -BlockingOnly` TWICE
             (CSV + JUnit) for the same reason.
          6. Computes the 3-bucket model that matches the
             Get-AzLocalClusterUpdateReadiness Summary:
             ReadyForUpdate / UpToDate / NotReady.
          7. Computes Critical-health bucket counts from the
             Test-AzLocalClusterHealth -PassThru row shape
             (ClusterName, HealthState, CriticalCount, WarningCount).
          8. Merges readiness.xml + health-blocking.xml into a single
             combined assess-readiness.xml (single Checks-tab entry).
          9. Emits the markdown step summary (8 sections: header tile,
             action banner, summary counts, Not-Ready table, Critical-
             health table, per-ring pivot, all-clusters detail,
             cross-link list) via `Add-AzLocalPipelineStepSummary`.
         10. Emits 2 step outputs via `Set-AzLocalPipelineOutput`:
             not_ready, critical_failures.
 
        Internal reuse (per the v0.8.5 thin-YAML consistency contract):
          * `Get-AzLocalClusterInventory` for the all-clusters scope and
            the UpdateRing pivot map.
          * `Get-AzLocalClusterUpdateReadiness` for the readiness CSV
            and JUnit XML.
          * `Test-AzLocalClusterHealth -BlockingOnly` for the blocking
            health CSV and JUnit XML.
          * `Add-AzLocalPipelineStepSummary` for the rendered markdown.
          * `Set-AzLocalPipelineOutput` for the step outputs.
          * `Get-AzLocalPipelineHost` is implicit (the above branch on it).
 
    .PARAMETER OutputDirectory
        Directory to write artifacts into. Created if it does not exist.
        Defaults to './artifacts' (GH / Local) or
        `$env:BUILD_ARTIFACTSTAGINGDIRECTORY` (Azure DevOps).
 
    .PARAMETER Scope
        'all' (default) - assess every cluster the identity can see (via
        Get-AzLocalClusterInventory). 'by-update-ring' - assess only
        clusters whose UpdateRing tag matches -UpdateRing.
 
    .PARAMETER UpdateRing
        UpdateRing tag value to filter by when -Scope is 'by-update-ring'.
        Accepts a single ring ('Wave1'), a semicolon-delimited list
        ('Prod;Ring2'), or '***' to match every cluster that HAS the
        UpdateRing tag set. Ignored when -Scope is 'all'.
 
    .PARAMETER ReadinessCsvFileName
        Filename for the per-cluster readiness CSV.
        Default 'readiness.csv'.
 
    .PARAMETER ReadinessXmlFileName
        Filename for the readiness JUnit XML report.
        Default 'readiness.xml'.
 
    .PARAMETER HealthCsvFileName
        Filename for the per-cluster blocking-health CSV.
        Default 'health-blocking.csv'.
 
    .PARAMETER HealthXmlFileName
        Filename for the blocking-health JUnit XML report.
        Default 'health-blocking.xml'.
 
    .PARAMETER CombinedXmlFileName
        Filename for the merged readiness + blocking-health JUnit report.
        Default 'assess-readiness.xml'.
 
    .PARAMETER SummaryFileName
        Per-task summary filename used by `Add-AzLocalPipelineStepSummary`
        on Azure DevOps and Local hosts.
        Default 'assess-readiness-summary.md'.
 
    .PARAMETER InstalledModuleVersion
        Optional [string] used in the markdown footer
        ('Generated by AzLocal.UpdateManagement v<x>').
 
    .PARAMETER PassThru
        When set, returns a single PSCustomObject summarising the run
        (TotalCount, ReadyForUpdateCount, UpToDateCount, NotReadyCount,
        CriticalFindings, ClustersWithCritical, ReadinessRows,
        HealthRows, and the 5 file paths). Without -PassThru the cmdlet
        emits nothing to the pipeline; the artifacts and step outputs
        are still produced.
 
    .OUTPUTS
        Nothing by default. When -PassThru is set, a single PSCustomObject.
 
    .EXAMPLE
        Export-AzLocalClusterUpdateReadinessReport -Scope all -PassThru
 
    .EXAMPLE
        Export-AzLocalClusterUpdateReadinessReport -Scope by-update-ring -UpdateRing 'Wave1'
 
    .NOTES
        Module: AzLocal.UpdateManagement (v0.8.5+)
        Roadmap: Step.5 - Assess Update Readiness (pre-flight gate).
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [string]$OutputDirectory,

        [Parameter(Mandatory = $false)]
        [ValidateSet('all', 'by-update-ring')]
        [string]$Scope = 'all',

        [Parameter(Mandatory = $false)]
        [AllowEmptyString()]
        [AllowNull()]
        [string]$UpdateRing,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [string]$ReadinessCsvFileName = 'readiness.csv',

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [string]$ReadinessXmlFileName = 'readiness.xml',

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [string]$HealthCsvFileName = 'health-blocking.csv',

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [string]$HealthXmlFileName = 'health-blocking.xml',

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [string]$CombinedXmlFileName = 'assess-readiness.xml',

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [string]$SummaryFileName = 'assess-readiness-summary.md',

        [Parameter(Mandatory = $false)]
        [AllowEmptyString()]
        [AllowNull()]
        [string]$InstalledModuleVersion,

        [Parameter(Mandatory = $false)]
        [switch]$PassThru
    )

    $pipelineHost = Get-AzLocalPipelineHost

    if (-not $OutputDirectory) {
        if ($pipelineHost -eq 'AzureDevOps' -and $env:BUILD_ARTIFACTSTAGINGDIRECTORY) {
            $OutputDirectory = $env:BUILD_ARTIFACTSTAGINGDIRECTORY
        }
        else {
            $OutputDirectory = './artifacts'
        }
    }
    if (-not (Test-Path -LiteralPath $OutputDirectory)) {
        New-Item -ItemType Directory -Path $OutputDirectory -Force | Out-Null
    }

    $readinessCsv = Join-Path -Path $OutputDirectory -ChildPath $ReadinessCsvFileName
    $readinessXml = Join-Path -Path $OutputDirectory -ChildPath $ReadinessXmlFileName
    $healthCsv    = Join-Path -Path $OutputDirectory -ChildPath $HealthCsvFileName
    $healthXml    = Join-Path -Path $OutputDirectory -ChildPath $HealthXmlFileName
    $combinedXml  = Join-Path -Path $OutputDirectory -ChildPath $CombinedXmlFileName

    # Always fetch inventory so we can build a ResourceId -> UpdateRing map
    # for the per-ring pivot in the markdown summary (cheap ARG round-trip).
    $inventory = Get-AzLocalClusterInventory -PassThru
    $ringByResourceId = @{}
    if ($inventory) {
        foreach ($inv in $inventory) {
            $ringValue = if ($inv.UpdateRing) { [string]$inv.UpdateRing } else { '(no ring tag)' }
            $ringByResourceId[$inv.ResourceId] = $ringValue
        }
    }

    # ---- Scope params -----------------------------------------------------
    $scopeParams = @{}
    if ($Scope -eq 'by-update-ring' -and $UpdateRing) {
        $scopeParams['ScopeByUpdateRingTag'] = $true
        $scopeParams['UpdateRingValue']      = $UpdateRing
        Write-Host "Scope: UpdateRing = $UpdateRing"
    }
    else {
        Write-Host "Scope: all clusters (via inventory)"
        if (-not $inventory -or @($inventory).Count -eq 0) {
            Write-Warning 'No clusters found in inventory.'
            Set-AzLocalPipelineOutput -Name 'not_ready'         -Value '0'
            Set-AzLocalPipelineOutput -Name 'critical_failures' -Value '0'
            $idleSb = New-Object 'System.Collections.Generic.List[string]'
            [void]$idleSb.Add('## Update Readiness Assessment')
            [void]$idleSb.Add('')
            [void]$idleSb.Add('**[IDLE]** No clusters found in inventory. Nothing to assess.')
            Add-AzLocalPipelineStepSummary -Markdown ($idleSb -join [Environment]::NewLine) -SummaryFileName $SummaryFileName | Out-Null
            if ($PassThru) {
                return [pscustomobject]@{
                    TotalCount           = 0
                    ReadyForUpdateCount  = 0
                    UpToDateCount        = 0
                    NotReadyCount        = 0
                    CriticalFindings     = 0
                    ClustersWithCritical = 0
                    ReadinessRows        = @()
                    HealthRows           = @()
                    ReadinessCsvPath     = $readinessCsv
                    ReadinessXmlPath     = $readinessXml
                    HealthCsvPath        = $healthCsv
                    HealthXmlPath        = $healthXml
                    CombinedXmlPath      = $combinedXml
                }
            }
            return
        }
        $scopeParams['ClusterResourceIds'] = @($inventory | Select-Object -ExpandProperty ResourceId)
    }

    Write-Host ''
    Write-Host '========================================'
    Write-Host 'Step 1: Readiness (Get-AzLocalClusterUpdateReadiness)'
    Write-Host '========================================'

    # CSV for humans
    $readiness = Get-AzLocalClusterUpdateReadiness @scopeParams `
        -ExportPath $readinessCsv `
        -PassThru

    # JUnit XML for the test reporter (ExportPath .xml auto-detects JUnitXml).
    # Two calls intentionally - this preserves the v0.8.4 dorny/test-reporter
    # contract byte-for-byte (the cmdlet's native JUnit shape is what operators
    # have screenshots / automations for). ARG round-trip is cheap.
    $null = Get-AzLocalClusterUpdateReadiness @scopeParams `
        -ExportPath $readinessXml

    # v0.7.99: 3-bucket model matches Get-AzLocalClusterUpdateReadiness Summary.
    # UpToDate clusters are NOT rolled into NotReady - they are a distinct bucket.
    $readyForUpdate = @($readiness | Where-Object { $_.ReadyForUpdate -eq $true }).Count
    $upToDate = @($readiness | Where-Object {
            $_.ReadyForUpdate -ne $true -and
            $_.UpdateState -in @('UpToDate', 'AppliedSuccessfully') -and
            [string]::IsNullOrEmpty([string]$_.AllAvailableUpdates)
        }).Count
    $total = @($readiness).Count
    $notReady = $total - $readyForUpdate - $upToDate

    Write-Host ''
    Write-Host "Total clusters in scope: $total"
    Write-Host "Ready for update : $readyForUpdate"
    Write-Host "Up to date : $upToDate"
    Write-Host "Not ready for update : $notReady"

    Write-Host ''
    Write-Host '========================================'
    Write-Host 'Step 2: Blocking health (Test-AzLocalClusterHealth -BlockingOnly)'
    Write-Host '========================================'

    $health = Test-AzLocalClusterHealth @scopeParams `
        -BlockingOnly `
        -ExportPath $healthCsv `
        -PassThru

    $null = Test-AzLocalClusterHealth @scopeParams `
        -BlockingOnly `
        -ExportPath $healthXml

    # ---- Combined JUnit XML ------------------------------------------------
    # Merge readiness.xml + health-blocking.xml into assess-readiness.xml so
    # operators get one Checks-tab entry instead of two. The individual XMLs
    # are still published below as [JUnit Debug] entries for parity.
    try {
        $readinessDoc = [xml](Get-Content -LiteralPath $readinessXml -Raw)
        $healthDoc    = [xml](Get-Content -LiteralPath $healthXml -Raw)
        $combinedDoc  = New-Object System.Xml.XmlDocument
        $declaration  = $combinedDoc.CreateXmlDeclaration('1.0', 'utf-8', $null)
        $combinedDoc.AppendChild($declaration) | Out-Null
        $rootElement  = $combinedDoc.CreateElement('testsuites')
        $rootElement.SetAttribute('name', 'Update Readiness Assessment')
        $combinedDoc.AppendChild($rootElement) | Out-Null
        foreach ($srcDoc in @($readinessDoc, $healthDoc)) {
            $suites = if ($srcDoc.DocumentElement.LocalName -eq 'testsuites') {
                $srcDoc.DocumentElement.SelectNodes('testsuite')
            }
            else {
                ,$srcDoc.DocumentElement
            }
            foreach ($suite in $suites) {
                $imported = $combinedDoc.ImportNode($suite, $true)
                $rootElement.AppendChild($imported) | Out-Null
            }
        }
        $combinedDoc.Save($combinedXml)
        Write-Host "Combined JUnit report: $combinedXml"
    }
    catch {
        Write-Warning "Failed to build combined JUnit report: $($_.Exception.Message)"
    }

    # Test-AzLocalClusterHealth -PassThru row shape (one row per cluster):
    # ClusterName, HealthState, Passed, CriticalCount, WarningCount, Failures
    # Aggregate from CriticalCount / Failures (NOT a non-existent Severity
    # property, which silently returned 0 in earlier yml versions).
    $criticalSum = ($health | Measure-Object -Property CriticalCount -Sum).Sum
    $criticalFindings = if ($criticalSum) { [int]$criticalSum } else { 0 }
    $clustersWithCritical = @($health | Where-Object { [int]$_.CriticalCount -gt 0 }).Count

    Write-Host ''
    Write-Host "Critical findings : $criticalFindings"
    Write-Host "Clusters with Critical : $clustersWithCritical"

    # ---- Step outputs -----------------------------------------------------
    Set-AzLocalPipelineOutput -Name 'not_ready'         -Value ([string]$notReady)
    Set-AzLocalPipelineOutput -Name 'critical_failures' -Value ([string]$clustersWithCritical)

    # ---- Markdown step summary (8 sections) -------------------------------
    $md = New-Object 'System.Collections.Generic.List[string]'
    [void]$md.Add('## Update Readiness Assessment')
    [void]$md.Add('')

    # 1. Header tile (one-line status, ASCII-safe brackets)
    $scopeLabel = $Scope
    if ($UpdateRing) { $scopeLabel = "$Scope (UpdateRing = $UpdateRing)" }
    $statusWord = if ($notReady -gt 0 -or $clustersWithCritical -gt 0) { 'ATTENTION' } else { 'OK' }
    [void]$md.Add("**[$statusWord]** $total cluster(s) assessed | $readyForUpdate Ready for Update | $upToDate Up to Date | $notReady Not Ready for Update | $clustersWithCritical with Critical health failures | Scope: $scopeLabel")
    [void]$md.Add('')

    # 2. Action banner
    if ($notReady -gt 0 -or $clustersWithCritical -gt 0) {
        [void]$md.Add("> **Action required**: $notReady cluster(s) not ready and/or $clustersWithCritical cluster(s) with Critical health failures. Review the **Not-Ready** and **Critical-health** sections below first; the CSV artifacts in ``azlocal-step.5-readiness-assessment-report_*`` carry the full per-finding detail. Remediate (hardware vendor SBE / firmware / cluster health) before or alongside the next apply-updates run. **The healthy clusters are safe to proceed** - Step.6_apply-updates.yml is per-cluster scoped.")
    }
    else {
        [void]$md.Add('> **All clear**: every cluster in scope is ready for update. Safe to proceed with Step.6_apply-updates.yml for this ring.')
    }
    [void]$md.Add('')

    # 3. Summary counts
    [void]$md.Add('### Summary counts')
    [void]$md.Add('')
    [void]$md.Add('| Metric | Count |')
    [void]$md.Add('|--------|-------|')
    [void]$md.Add("| Total clusters in scope | $total |")
    [void]$md.Add("| Ready for update | $readyForUpdate |")
    [void]$md.Add("| Up to date | $upToDate |")
    [void]$md.Add("| Not ready for update | $notReady |")
    [void]$md.Add("| Clusters with Critical health failures | $clustersWithCritical |")
    [void]$md.Add("| Total Critical findings | $criticalFindings |")
    [void]$md.Add('')

    # 4. Not-Ready cluster table (blocking findings first)
    $notReadyRows = @($readiness | Where-Object { $_.ReadyForUpdate -ne $true })
    if ($notReadyRows.Count -gt 0) {
        [void]$md.Add('### Not-Ready clusters (review first)')
        [void]$md.Add('')
        [void]$md.Add('| Cluster | UpdateRing | Current version | Update state | Health | Blocking reasons |')
        [void]$md.Add('|---------|------------|-----------------|--------------|--------|------------------|')
        foreach ($r in ($notReadyRows | Sort-Object @{Expression={ if ($ringByResourceId.ContainsKey($_.ClusterResourceId)) { $ringByResourceId[$_.ClusterResourceId] } else { 'zzz' } }}, ClusterName)) {
            $ring = if ($ringByResourceId.ContainsKey($r.ClusterResourceId)) { $ringByResourceId[$r.ClusterResourceId] } else { '-' }
            $cv = if ($r.CurrentVersion) { $r.CurrentVersion } else { '-' }
            $br = if ($r.PSObject.Properties['BlockingReasons'] -and $r.BlockingReasons) { $r.BlockingReasons } else { '-' }
            [void]$md.Add("| $($r.ClusterName) | $ring | $cv | $($r.UpdateState) | $($r.HealthState) | $br |")
        }
        [void]$md.Add('')
    }

    # 5. Critical-health cluster table
    $criticalRows = @($health | Where-Object { [int]$_.CriticalCount -gt 0 })
    if ($criticalRows.Count -gt 0) {
        [void]$md.Add('### Critical-health clusters')
        [void]$md.Add('')
        [void]$md.Add('_Cross-link: see **Step.4_fleet-connectivity-status** for connectivity-class failures and **Step.9_fleet-health-status** for the broader Critical/Warning catalog._')
        [void]$md.Add('')
        [void]$md.Add('| Cluster | UpdateRing | Health state | Critical | Warning |')
        [void]$md.Add('|---------|------------|--------------|----------|---------|')
        foreach ($r in ($criticalRows | Sort-Object @{Expression={[int]$_.CriticalCount}; Descending=$true}, ClusterName)) {
            $invMatch = $inventory | Where-Object { $_.ClusterName -eq $r.ClusterName } | Select-Object -First 1
            $ring = if ($invMatch -and $invMatch.UpdateRing) { $invMatch.UpdateRing } else { '-' }
            [void]$md.Add("| $($r.ClusterName) | $ring | $($r.HealthState) | $($r.CriticalCount) | $($r.WarningCount) |")
        }
        [void]$md.Add('')
    }

    # 6. Per-UpdateRing pivot (only when >1 ring in scope)
    $ringGroups = $readiness | Group-Object @{Expression={ if ($ringByResourceId.ContainsKey($_.ClusterResourceId)) { $ringByResourceId[$_.ClusterResourceId] } else { '(no ring tag)' } }} | Sort-Object Name
    if (@($ringGroups).Count -gt 1) {
        [void]$md.Add('### Per UpdateRing breakdown')
        [void]$md.Add('')
        [void]$md.Add('| UpdateRing | Total | Ready for Update | Up to Date | Not Ready for Update |')
        [void]$md.Add('|------------|-------|------------------|------------|----------------------|')
        foreach ($g in $ringGroups) {
            $gReady = @($g.Group | Where-Object { $_.ReadyForUpdate -eq $true }).Count
            $gUpToDate = @($g.Group | Where-Object {
                    $_.ReadyForUpdate -ne $true -and
                    $_.UpdateState -in @('UpToDate', 'AppliedSuccessfully') -and
                    [string]::IsNullOrEmpty([string]$_.AllAvailableUpdates)
                }).Count
            $gNotReady = $g.Count - $gReady - $gUpToDate
            [void]$md.Add("| $($g.Name) | $($g.Count) | $gReady | $gUpToDate | $gNotReady |")
        }
        [void]$md.Add('')
    }

    # 7. All-clusters detail table
    if ($total -gt 0) {
        [void]$md.Add('### All clusters detail')
        [void]$md.Add('')
        [void]$md.Add('| Cluster | UpdateRing | Current version | Current SBE version | Update state | Health | Ready | Recommended update |')
        [void]$md.Add('|---------|------------|-----------------|---------------------|--------------|--------|-------|--------------------|')
        foreach ($r in ($readiness | Sort-Object @{Expression={ if ($ringByResourceId.ContainsKey($_.ClusterResourceId)) { $ringByResourceId[$_.ClusterResourceId] } else { 'zzz' } }}, ClusterName)) {
            $ring = if ($ringByResourceId.ContainsKey($r.ClusterResourceId)) { $ringByResourceId[$r.ClusterResourceId] } else { '-' }
            $cv  = if ($r.CurrentVersion) { $r.CurrentVersion } else { '-' }
            $csv = if ($r.PSObject.Properties['CurrentSbeVersion'] -and $r.CurrentSbeVersion) { $r.CurrentSbeVersion } else { '-' }
            $ru  = if ($r.RecommendedUpdate) { $r.RecommendedUpdate } else { '-' }
            [void]$md.Add("| $($r.ClusterName) | $ring | $cv | $csv | $($r.UpdateState) | $($r.HealthState) | $($r.ReadyForUpdate) | $ru |")
        }
        [void]$md.Add('')
    }

    # 8. Cross-links to other pipelines
    [void]$md.Add('### Cross-link to other pipelines')
    [void]$md.Add('')
    [void]$md.Add('- **Step.4_fleet-connectivity-status** - root-cause Disconnected / Offline / partial-connectivity findings on the Not-Ready and Critical-health rows above.')
    [void]$md.Add('- **Step.6_apply-updates** - apply updates to the Ready clusters in this ring (manual workflow_dispatch, or wait for the scheduled cron firing).')
    [void]$md.Add('- **Step.7_monitor-updates** - tail in-flight runs once Step.6 has started (auto-trigger on Step.6 completion, or manual).')
    [void]$md.Add('- **Step.9_fleet-health-status** - broader Critical / Warning health catalog across the whole fleet (not just blocking-only).')
    [void]$md.Add('')
    [void]$md.Add('_Note: the **Update Readiness Assessment** entry in the Checks tab is the merged combined view; the [JUnit Debug] entries are diagnostic mirrors for CI/test tooling._')

    if ($InstalledModuleVersion) {
        [void]$md.Add('')
        [void]$md.Add(('_Generated by AzLocal.UpdateManagement v{0}._' -f $InstalledModuleVersion))
    }

    Add-AzLocalPipelineStepSummary -Markdown ($md -join [Environment]::NewLine) -SummaryFileName $SummaryFileName | Out-Null

    if ($PassThru) {
        return [pscustomobject]@{
            TotalCount           = [int]$total
            ReadyForUpdateCount  = [int]$readyForUpdate
            UpToDateCount        = [int]$upToDate
            NotReadyCount        = [int]$notReady
            CriticalFindings     = [int]$criticalFindings
            ClustersWithCritical = [int]$clustersWithCritical
            ReadinessRows        = @($readiness)
            HealthRows           = @($health)
            ReadinessCsvPath     = $readinessCsv
            ReadinessXmlPath     = $readinessXml
            HealthCsvPath        = $healthCsv
            HealthXmlPath        = $healthXml
            CombinedXmlPath      = $combinedXml
        }
    }
}