Common/Import-CmmcHandoff.ps1

<#
.SYNOPSIS
    Loads the CMMC EZ-CMMC handoff artifact for the report layer.
.DESCRIPTION
    Loads controls/cmmc-ez-handoff.json (synced from CheckID via CI) and
    returns the parsed content plus a derived per-level summary of
    classifications (out-of-scope / partial / coverable / inherent).

    The handoff artifact catalogues CMMC 2.0 practices that M365-Assess
    does not automate — either because there is no M365 configuration
    equivalent (physical access, HR), because M365 partially addresses
    the practice, because a future check could address it, or because
    M365 satisfies it inherently without configuration.

    Returns $null when the file is missing so the report layer can
    degrade gracefully for older installations.
.PARAMETER ControlsPath
    Path to the controls/ directory containing cmmc-ez-handoff.json.
.OUTPUTS
    [hashtable] with keys SchemaVersion, Generated, Description, Coverage,
    Practices (array of practice objects), Summary (per-level counts).
    Returns $null if the handoff file is not present.
.EXAMPLE
    Import-CmmcHandoff -ControlsPath "$PSScriptRoot\..\controls"
#>

function Import-CmmcHandoff {
    [CmdletBinding()]
    [OutputType([hashtable])]
    param(
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$ControlsPath
    )

    $handoffPath = Join-Path -Path $ControlsPath -ChildPath 'cmmc-ez-handoff.json'
    if (-not (Test-Path -Path $handoffPath)) {
        Write-Verbose "CMMC handoff file not found: $handoffPath"
        return $null
    }

    $raw = Get-Content -Path $handoffPath -Raw | ConvertFrom-Json
    $practices = @($raw.practices)

    # Pre-compute summary so the React layer just reads pills, not aggregates.
    # Level keys match the handoff schema (L1/L2/L3); classification keys are
    # camelCased (JS convention) because this hashtable is serialized to JSON
    # and consumed by the browser.
    $newBucket = { [ordered]@{ outOfScope = 0; partial = 0; coverable = 0; inherent = 0 } }
    $summary = [ordered]@{
        L1    = & $newBucket
        L2    = & $newBucket
        L3    = & $newBucket
        Total = & $newBucket
    }
    $summary.Total['practices'] = $practices.Count

    foreach ($practice in $practices) {
        $bucketKey = switch ($practice.classification) {
            'out-of-scope' { 'outOfScope' }
            'partial'      { 'partial' }
            'coverable'    { 'coverable' }
            'inherent'     { 'inherent' }
            default        { $null }
        }
        if ($null -eq $bucketKey) { continue }
        if (-not $summary.Contains($practice.level)) { continue }

        $summary[$practice.level][$bucketKey]++
        $summary.Total[$bucketKey]++
    }

    return @{
        SchemaVersion = $raw.schemaVersion
        Generated     = $raw.generated
        Description   = $raw.description
        Coverage      = $raw.coverage
        Practices     = $practices
        Summary       = $summary
    }
}