scripts/internal/continuous-co-review/reviewer-selection-policy.ps1

$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest

function Select-ContinuousCoReviewReviewerCandidate {
    param(
        [Parameter(Mandatory)]
        $Catalog,

        [AllowNull()]
        [string] $RequestedHost,

        [AllowNull()]
        [string] $RequestedModel,

        [AllowNull()]
        [string] $CodeWriterHost,

        [switch] $FallbackOnly
    )

    $eligible = @(
        foreach ($candidate in @($Catalog.hosts)) {
            if (-not [bool] $candidate.allowed) { continue }
            if (-not [bool] $candidate.installed) { continue }
            if ($FallbackOnly -and -not [bool] $candidate.fallback_allowed) { continue }
            if (-not [string]::IsNullOrWhiteSpace($RequestedHost) -and $candidate.host -ne $RequestedHost) { continue }
            if (-not [string]::IsNullOrWhiteSpace($RequestedModel) -and $candidate.model -ne $RequestedModel) { continue }

            $authorization = Test-ContinuousCoReviewReviewerAuthorization -Candidate $candidate
            if (-not $authorization.authorized) { continue }

            [pscustomobject][ordered]@{
                host                       = $candidate.host
                model                      = $candidate.model
                adapter_id                 = $candidate.adapter_id
                review_class_rank          = [int] $candidate.review_class_rank
                model_source               = $candidate.model_source
                cost_class                 = $candidate.cost_class
                authorization_ref          = $authorization.authorization_ref
                fallback_allowed           = [bool] $candidate.fallback_allowed
                authorized                 = $true
                exact_alternate_authorized = [bool] $candidate.fallback_allowed
                timeout_seconds            = [int] (Get-ContinuousCoReviewCatalogValue -Object $candidate -Name 'timeout_seconds' -DefaultValue 30)
                selection_reason           = 'highest-authorized-review-class-rank'
            }
        }
    )

    $rankedEligible = @($eligible | Sort-Object -Property @{ Expression = 'review_class_rank'; Descending = $true }, @{ Expression = 'host'; Descending = $false })
    # HOST-NEUTRAL independence preference (D-197-I010-002 / FR-016+SEC-004): prefer the STRONGEST
    # eligible candidate on a DIFFERENT harness than the code-writer - a pure policy over catalog
    # data, valid for any host including ones added later. Host specifics (names, binaries, flags)
    # live ONLY in reviewer-host-catalog.ps1; this core never names a harness.
    $preferredIndependent = $false
    $selection = @()
    if ([string]::IsNullOrWhiteSpace($RequestedHost) -and -not [string]::IsNullOrWhiteSpace($CodeWriterHost)) {
        $selection = @($rankedEligible | Where-Object { $_.host -ne $CodeWriterHost } | Select-Object -First 1)
        if (@($selection).Count -gt 0) { $preferredIndependent = $true }
    }

    if (@($selection).Count -eq 0) {
        $selection = @($rankedEligible | Select-Object -First 1)
    }

    if (@($selection).Count -eq 0) {
        return $null
    }

    if (-not [string]::IsNullOrWhiteSpace($RequestedHost)) {
        # honour-or-surface --host (T093/FR-035): an explicit request is HONOURED (the filter above
        # restricted eligibility to it); the caller surfaces the reason when it cannot be.
        $selection[0].selection_reason = 'requested-host-honoured'
    }
    elseif ($preferredIndependent) {
        $selection[0].selection_reason = 'preferred-independent-reviewer-for-code-writer-host'
    }

    # T093/FR-035 + SEC-004 (iter-009 D1): INDEPENDENCE is first-class selection evidence - a same-host
    # review fires immediately as a LABELLED fallback (never blocks, never silently substitutes), and
    # the label feeds the D4 tiered-evidence gate. 'unverified' = the code-writer host is unknown, so
    # independence cannot be asserted (the gate treats it as NOT independent - conservative).
    $independence = if ([string]::IsNullOrWhiteSpace($CodeWriterHost)) { 'unverified' }
    elseif ($selection[0].host -ne $CodeWriterHost) { 'independent' }
    else { 'same-host' }
    $selection[0] | Add-Member -NotePropertyName 'independence' -NotePropertyValue $independence -Force
    if ($independence -eq 'same-host' -and [string]::IsNullOrWhiteSpace($RequestedHost)) {
        $selection[0].selection_reason = 'same-host-fallback-no-independent-authorized'
    }

    return $selection[0]
}