Private/Read-KadenOperatorModelFile.ps1

function Read-KadenOperatorModelFile {
    <#
    .SYNOPSIS
        Read the operator model file without writing it.
    .DESCRIPTION
        A missing file is a skip. A file that cannot be parsed is a failure.
        Known host stacks are cursor, copilot, and claude. Extra fields are ignored.
        An empty model value is a per-stack skip. The operator file is never modified.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$LiteralPath
    )

    $known = @('cursor', 'copilot', 'claude')
    if (-not (Test-Path -LiteralPath $LiteralPath -PathType Leaf)) {
        return [pscustomobject]@{
            Status  = 'skipped'
            Reason  = 'no operator file exists'
            Reports = @(New-KadenUntunedStackReport -Names $known)
        }
    }

    try {
        $raw = [System.IO.File]::ReadAllText($LiteralPath)
        $json = $raw | ConvertFrom-Json
    } catch {
        return [pscustomobject]@{
            Status  = 'failed'
            Reason  = "operator file cannot be read or parsed: $($_.Exception.Message)"
            Reports = @()
        }
    }

    if ($null -eq $json -or $null -eq $json.PSObject.Properties['stacks']) {
        return [pscustomobject]@{
            Status  = 'failed'
            Reason  = 'operator file cannot be read or parsed: stacks object is missing'
            Reports = @()
        }
    }
    $stacksValue = $json.stacks
    if ($null -eq $stacksValue -or $stacksValue -isnot [System.Management.Automation.PSCustomObject]) {
        return [pscustomobject]@{
            Status  = 'failed'
            Reason  = 'operator file cannot be read or parsed: stacks must be an object'
            Reports = @()
        }
    }

    $reports = New-Object System.Collections.Generic.List[object]
    $declared = @{}
    foreach ($prop in @($stacksValue.PSObject.Properties)) {
        $declared[$prop.Name] = $prop.Value
    }

    foreach ($name in $known) {
        if (-not $declared.ContainsKey($name)) {
            $reports.Add((New-KadenStackReport -Name $name -Disposition 'untuned' -Model $null -Reason 'no operator entry - left untuned'))
            continue
        }
        $reports.Add((ConvertTo-KadenStackChoice -Name $name -Entry $declared[$name]))
    }

    foreach ($name in @($declared.Keys)) {
        if ($known -contains $name) { continue }
        $reports.Add((New-KadenStackReport -Name $name -Disposition 'skipped' -Model $null -Reason 'unknown host stack - left untouched'))
    }

    $reportArray = @($reports.ToArray())
    $tuned = @($reportArray | Where-Object { $_.Disposition -eq 'tuned' })
    $status = if ($tuned.Count -gt 0) { 'ready' } else { 'skipped' }
    $reason = if ($status -eq 'ready') {
        'operator choices are ready to apply'
    } else {
        'no operator entry - left untuned'
    }
    return [pscustomobject]@{
        Status  = $status
        Reason  = $reason
        Reports = $reportArray
    }
}