Public/Invoke-KriticalLensSchemaCompleteness.ps1

function Invoke-KriticalLensSchemaCompleteness {
<#
.SYNOPSIS
    Walks a Microsoft365DSC schema inventory against a target PowerShell
    module set and proves every resource, every parameter, and every
    AllowedValue is covered with correct type, Mandatory, and ValidateSet
    fidelity.

.DESCRIPTION
    Kritical.Lens.SchemaCompleteness is the first standalone slice of the
    Kritical Lens family — the collective intelligence layer that lets any
    consumer prove a claim about a codebase with hard evidence rather than
    marketing words.

    Six gates run per resource:

      C1 Primitive exists (`New-<Prefix><Resource>` found)
      C2 Every schema parameter present in the primitive's param block
      C3 Mandatory attribute matches the schema
      C4 DataType maps to a compatible PowerShell type family
      C5 ValidateSet covers every AllowedValue from the schema
      C6 Read-side accessor exists (`Get-<Prefix><Resource>` found)

    The auditor discovers primitive functions by scanning target modules
    for the marker comment `# COVERS-DSC-RESOURCE: <ResourceName>`
    immediately preceding a function definition. Modules that follow that
    convention drop straight in.

.PARAMETER InventoryPath
    Path to the Microsoft365DSC schema inventory JSON — the file produced
    by walking `.schema.mof` files and emitting one object per resource
    with a Parameters array of name, DataType, Mandatory, AllowedValues.

.PARAMETER ModuleDir
    Directory containing the target `.psm1` modules whose function-set is
    being audited for schema parity.

.PARAMETER ModuleFilter
    Optional glob filter for module filenames. Defaults to `Kritical-M365*.psm1`.

.PARAMETER FunctionPrefix
    Function-name prefix expected on covering functions (for example the
    prefix `Kritical` in `New-KriticalAADUser`). Defaults to `Kritical`.

.PARAMETER SupersetHint
    Optional substring that identifies the schema-derived superset variant
    of a function name. When present, that variant is preferred over
    hand-crafted alternatives for the audit. Defaults to `Dsc`.

.PARAMETER OutputMd
    Path to the Markdown verdict file. When omitted, no Markdown is emitted.

.PARAMETER OutputJson
    Path to the machine-readable JSON detail file. When omitted, no JSON
    is emitted.

.OUTPUTS
    PSCustomObject with fields Summary, ByClass, Rows, MdPath, JsonPath.

.EXAMPLE
    Invoke-KriticalLensSchemaCompleteness `
        -InventoryPath ./m365dsc-schema-inventory.json `
        -ModuleDir ./src/modules `
        -OutputMd ./reports/schema-completeness.md `
        -OutputJson ./reports/schema-completeness.json
#>

    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param(
        [Parameter(Mandatory)][string]$InventoryPath,
        [Parameter(Mandatory)][string]$ModuleDir,
        [string]$ModuleFilter   = 'Kritical-M365*.psm1',
        [string]$FunctionPrefix = 'Kritical',
        [string]$SupersetHint   = 'Dsc',
        [string]$OutputMd,
        [string]$OutputJson
    )

    if (-not (Test-Path -LiteralPath $InventoryPath)) {
        throw "Schema inventory not found: $InventoryPath"
    }
    if (-not (Test-Path -LiteralPath $ModuleDir)) {
        throw "Module directory not found: $ModuleDir"
    }

    Write-Verbose ("Kritical.Lens.SchemaCompleteness: loading inventory {0}" -f $InventoryPath)
    $inv = Get-Content -LiteralPath $InventoryPath -Raw | ConvertFrom-Json

    $resources = @($inv.Resources | Where-Object { $_.ResourceName })
    Write-Verbose ("Kritical.Lens.SchemaCompleteness: {0} resources in inventory" -f $resources.Count)

    $primitives = _KriticalLensIndexPrimitives `
        -ModuleDir      $ModuleDir `
        -ModuleFilter   $ModuleFilter `
        -FunctionPrefix $FunctionPrefix `
        -SupersetHint   $SupersetHint

    Write-Verbose ("Kritical.Lens.SchemaCompleteness: {0} primitives indexed" -f $primitives.Count)

    $summary = @{
        C1 = @{ Pass = 0; Fail = 0 }
        C2 = @{ Pass = 0; Fail = 0 }
        C3 = @{ Pass = 0; Fail = 0 }
        C4 = @{ Pass = 0; Fail = 0 }
        C5 = @{ Pass = 0; Fail = 0 }
        C6 = @{ Pass = 0; Fail = 0 }
    }
    $rows = @()

    foreach ($r in ($resources | Sort-Object ResourceName)) {
        $prim = $primitives[$r.ResourceName]
        $eval = _KriticalLensEvaluateResource -Resource $r -Primitive $prim

        if ($eval.C1) { $summary.C1.Pass++ } else { $summary.C1.Fail++ }
        if ($eval.C6) { $summary.C6.Pass++ } else { $summary.C6.Fail++ }

        foreach ($pp in $eval.PerParam) {
            if ($pp.C2) { $summary.C2.Pass++ } else { $summary.C2.Fail++ }
            if ($pp.PSObject.Properties['C3'] -and $pp.C3 -ne $null) {
                if ($pp.C3) { $summary.C3.Pass++ } else { $summary.C3.Fail++ }
            }
            if ($pp.PSObject.Properties['C4'] -and $pp.C4 -ne $null) {
                if ($pp.C4) { $summary.C4.Pass++ } else { $summary.C4.Fail++ }
            }
            if ($pp.PSObject.Properties['C5'] -and $pp.C5 -ne $null) {
                if ($pp.C5) { $summary.C5.Pass++ } else { $summary.C5.Fail++ }
            }
        }

        foreach ($f in $eval.Findings) { $rows += $f }
    }

    $paramTotal    = ($resources | ForEach-Object { $_.Parameters.Count } | Measure-Object -Sum).Sum
    $resourceTotal = $resources.Count

    $byClass = $rows | Group-Object Class | Sort-Object Count -Descending | ForEach-Object {
        [pscustomobject]@{ Class = $_.Name; Count = $_.Count }
    }

    # Optional Markdown emit
    if ($OutputMd) {
        $md = New-Object System.Text.StringBuilder
        $null = $md.AppendLine('# Kritical Lens — schema completeness verdict')
        $null = $md.AppendLine('')
        $null = $md.AppendLine('## Universe')
        $null = $md.AppendLine(('- Resources in schema inventory: **{0}**' -f $resourceTotal))
        $null = $md.AppendLine(('- Parameter instances in schema: **{0}**' -f $paramTotal))
        $null = $md.AppendLine(('- Kritical primitives indexed: **{0}**' -f $primitives.Count))
        $null = $md.AppendLine('')
        $null = $md.AppendLine('## Per-gate verdict')
        $null = $md.AppendLine('')
        $null = $md.AppendLine('| Gate | Description | Pass | Fail | Ready % |')
        $null = $md.AppendLine('|---|---|---:|---:|---:|')
        $descs = @{
            'C1' = 'Primitive exists (New-)'
            'C2' = 'Every schema param present in Kritical param block'
            'C3' = 'Mandatory attribute matches schema'
            'C4' = 'DataType maps to compatible PowerShell type'
            'C5' = 'ValidateSet covers every AllowedValue'
            'C6' = 'Read-side accessor exists (Get-)'
        }
        foreach ($g in 'C1','C2','C3','C4','C5','C6') {
            $p = $summary[$g].Pass; $f = $summary[$g].Fail
            $tot = $p + $f
            $pct = if ($tot) { [math]::Round(($p / $tot) * 100, 1) } else { 0 }
            $null = $md.AppendLine(('| {0} | {1} | {2} | {3} | {4}% |' -f $g, $descs[$g], $p, $f, $pct))
        }
        $null = $md.AppendLine('')
        if ($byClass) {
            $null = $md.AppendLine('## Top gap classes')
            $null = $md.AppendLine('')
            $null = $md.AppendLine('| Class | Count |')
            $null = $md.AppendLine('|---|---:|')
            foreach ($c in $byClass) {
                $null = $md.AppendLine(('| {0} | {1} |' -f $c.Class, $c.Count))
            }
            $null = $md.AppendLine('')
        }
        if ($rows.Count -gt 0) {
            $null = $md.AppendLine('## First 100 actionable findings')
            $null = $md.AppendLine('')
            $null = $md.AppendLine('| Resource | Param | Class | Detail |')
            $null = $md.AppendLine('|---|---|---|---|')
            $take = [Math]::Min($rows.Count, 100)
            for ($i = 0; $i -lt $take; $i++) {
                $r = $rows[$i]
                $d = $r.Detail
                if ($d.Length -gt 120) { $d = $d.Substring(0,117) + '...' }
                $null = $md.AppendLine(('| {0} | {1} | {2} | {3} |' -f $r.Resource, $r.Param, $r.Class, $d))
            }
        }
        Set-Content -LiteralPath $OutputMd -Value $md.ToString() -Encoding utf8
    }

    # Optional JSON emit
    if ($OutputJson) {
        $obj = [ordered]@{
            Generator = 'Kritical.Lens.SchemaCompleteness'
            Version   = '1.0.0'
            Utc       = (Get-Date).ToUniversalTime().ToString('o')
            Universe  = @{ Resources = $resourceTotal; Parameters = $paramTotal; Primitives = $primitives.Count }
            Summary   = $summary
            ByClass   = $byClass
            Rows      = @($rows)
        }
        $obj | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $OutputJson -Encoding utf8
    }

    [pscustomobject]@{
        Summary  = $summary
        ByClass  = @($byClass)
        Rows     = @($rows)
        MdPath   = $OutputMd
        JsonPath = $OutputJson
    }
}