src/assess/Invoke-Assessment.ps1

#Requires -Version 7.0
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

<#
.SYNOPSIS
    Run every rule in every rule set against the collect object.
 
.NOTES
    Appends benchmark findings when a benchmark is supplied. Tracks ADO Story AB#5035.
#>

function Invoke-Assessment {
    param($Collect, $RuleSet, $Benchmark, [string] $Assessment)

    $findings = foreach ($set in $RuleSet) {
        # A rule file may declare a data prerequisite (`requires:`). When it does and NOT ONE of
        # the declared paths returned a row, the whole set is reported Unknown with the missing
        # datasets named -- never scored (AB#6832). Scoring it would turn "nothing was collected"
        # into "everything passed", which for a migration-readiness assessment on an estate with no
        # Azure Migrate project is the most misleading answer available.
        #
        # One satisfied path is enough: a tenant part-way through a migration legitimately has
        # projects but no Data Box, and that partial state is a finding, not a blocker.
        $Required = @(if ($set.PSObject.Properties['Requires']) { @($set.Requires) } else { @() })
        $Missing = @()
        if ($Required.Count -gt 0) {
            $Satisfied = $false
            foreach ($Requirement in $Required) {
                $Path = if ($Requirement -is [System.Collections.IDictionary]) { [string]$Requirement['path'] } else { [string]$Requirement.path }
                $Description = if ($Requirement -is [System.Collections.IDictionary]) { [string]$Requirement['description'] } else { [string]$Requirement.description }
                # NOT wrapped in @(): Resolve-JsonPath returns its array through
                # `Write-Output -NoEnumerate`, so @() would produce a one-element array holding the
                # (possibly empty) result and every prerequisite would read as satisfied.
                $Rows = $null
                try { $Rows = Resolve-JsonPath -InputObject $Collect -Path $Path } catch { $Rows = $null }
                $RowCount = if ($null -eq $Rows) { 0 } else { $Rows.Count }
                if ($RowCount -gt 0) { $Satisfied = $true } else { $Missing += $(if ($Description) { $Description } else { $Path }) }
            }
            if (-not $Satisfied) {
                foreach ($rule in $set.Rules) {
                    [pscustomobject]@{
                        Id            = $rule.id
                        Title         = $rule.title
                        Framework     = $set.Framework
                        Area          = $set.Area
                        Severity      = $rule.severity
                        Status        = 'Unknown'
                        EvidenceCount = 0
                        Evidence      = @()
                        Remediation   = "Not assessed: none of this rule set's required datasets returned any rows ($($Missing -join '; ')). Collect them before reading a score from this area."
                        Manual        = [bool]$rule.manual
                    } | Add-Member -NotePropertyName Assessment -NotePropertyValue $Assessment -PassThru |
                        Add-Member -NotePropertyName AreaWeight -NotePropertyValue $set.Weight -PassThru
                }
                continue
            }
        }

        foreach ($rule in $set.Rules) {
            $f = Invoke-Rule -Rule $rule -Collect $Collect -Area $set.Area -Framework $set.Framework
            $f | Add-Member -NotePropertyName Assessment -NotePropertyValue $Assessment -PassThru |
                 Add-Member -NotePropertyName AreaWeight -NotePropertyValue $set.Weight -PassThru
        }
    }
    if ($Benchmark) { $findings += Compare-Benchmark -Collect $Collect -Benchmark $Benchmark }
    return $findings
}