scripts/internal/continuous-co-review/review-signoff-evidence-gate.ps1

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

# T067 / FR-025: the deterministic co-review gate-floor decision (re-architected).
#
# "You cannot sign off on un-reviewed state." The first model (diff_hash recomputed from an
# operator-chosen baseline) was found unsound by the feature's own dogfooded co-reviews:
# HOLE A (gitignored source invisible) and HOLE B (the operator baseline was never verified
# as reviewed). The sound model:
# 1. FRESHNESS - the CURRENT reviewed-state tree-id (content-addressed; includes tracked,
# untracked, and gitignored source minus secrets) must equal a passing run's recorded
# reviewed_tree_id. (Closes HOLE A + the untracked/empty/diff-parsing nits.)
# 2. COVERAGE - that run's chain must reach the merge-base-with-trunk anchor with no gap,
# so everything the feature added on top of shipped trunk was reviewed. (Closes HOLE B.)
# 3. FAIL-CLOSED on every git/digest failure; an empty reviewed state never counts as fresh.
# 4. The only escape is a human-authorized, RECORDED partial-coverage override - never silent.
#
# This is the DECISION logic only. Wiring it into Invoke-SpecrewBoundaryStateSync as a
# throw-to-refuse gate stays deferred until the F-185 host-neutral gate-enforcement branch
# merges; Assert-ContinuousCoReviewSignoffGate is the thin throw-wrapper that wiring will call.

function New-ContinuousCoReviewSignoffGateDecision {
    param(
        [Parameter(Mandatory)]
        [ValidateSet('allow', 'block')]
        [string] $Decision,

        [Parameter(Mandatory)]
        [string] $Reason,

        [Parameter(Mandatory)]
        [string] $Message,

        [AllowNull()] [string] $CurrentTreeId,
        [AllowNull()] [string] $MatchedRunId,
        [AllowNull()] [string] $AnchorRef,
        [AllowNull()] $OverrideAuthorization,
        [AllowNull()] $EvidenceLabels,
        [AllowNull()] $Acknowledgement
    )

    return [pscustomobject][ordered]@{
        schema_version = '1.0'
        decision       = $Decision
        reason         = $Reason
        message        = $Message
        current_tree_id = $CurrentTreeId
        matched_run_id = $MatchedRunId
        anchor_ref     = $AnchorRef
        override       = $OverrideAuthorization
        evidence_labels = $EvidenceLabels
        acknowledgement = $Acknowledgement
    }
}

function Get-ContinuousCoReviewRunEvidenceLabels {
    # T094/FR-036 (iter-009 D4): a run record's 3-dimension assurance labels with CONSERVATIVE
    # defaults for records that predate the labels: completeness 'full' (promotion always required an
    # affirmative full pass), independence 'unverified' (unprovable -> not independent, SEC-004),
    # budget 'normal' ('time-extended' is NOT reduced assurance either way).
    param([AllowNull()] $Run)
    $labels = [pscustomobject]@{ completeness = 'full'; independence = 'unverified'; budget = 'normal' }
    if ($null -eq $Run) { return $labels }
    $recorded = Get-ContinuousCoReviewRunIndexProperty -Object $Run -Name 'evidence_labels'
    if ($null -eq $recorded) { return $labels }
    foreach ($dim in @('completeness', 'independence', 'budget')) {
        $val = [string](Get-ContinuousCoReviewRunIndexProperty -Object $recorded -Name $dim)
        if (-not [string]::IsNullOrWhiteSpace($val)) { $labels.$dim = $val }
    }
    return $labels
}

function Test-ContinuousCoReviewEvidenceIsDegraded {
    # D4 tiers: full + independent (any budget) is FULL assurance; anything else (partial OR a
    # not-provably-independent reviewer) is DEGRADED and needs a recorded human ack.
    param([Parameter(Mandatory)] $Labels)
    return (([string]$Labels.completeness -ne 'full') -or ([string]$Labels.independence -ne 'independent'))
}

function Add-ContinuousCoReviewDegradedAck {
    <#
        T094/FR-036: record the FIRST-CLASS human acknowledgement of degraded review evidence, as a
        durable per-run artifact (.specrew/review/inline/<run-id>/degraded-ack.json) the gate reads.
        TRUST BOUNDARY (same as the override + review-run.json, see Test-...OverrideAuthorization):
        construct only from a genuinely human-authored action (the `specrew review --ack-degraded`
        command / a captured human verdict), never from agent-forgeable input.
    #>

    param(
        [Parameter(Mandatory)][string] $RepoRoot,
        [Parameter(Mandatory)][string] $RunId,
        [Parameter(Mandatory)][string] $AuthorizedBy,
        [Parameter(Mandatory)][string] $Rationale,
        [datetime] $Now = [datetime]::UtcNow
    )
    if ([string]::IsNullOrWhiteSpace($AuthorizedBy) -or [string]::IsNullOrWhiteSpace($Rationale)) {
        throw 'Add-ContinuousCoReviewDegradedAck: -AuthorizedBy and -Rationale are both required (an ack is never implicit).'
    }
    $dir = Join-Path (Resolve-Path -LiteralPath $RepoRoot).Path ".specrew/review/inline/$RunId"
    New-Item -ItemType Directory -Path $dir -Force | Out-Null
    $ack = [pscustomobject][ordered]@{
        schema_version = '1.0'
        run_id         = $RunId
        authorized_by  = $AuthorizedBy
        rationale      = $Rationale
        acknowledged_at = $Now.ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ', [System.Globalization.CultureInfo]::InvariantCulture)
    }
    $path = Join-Path $dir 'degraded-ack.json'
    Set-Content -LiteralPath $path -Value ($ack | ConvertTo-Json -Depth 8) -Encoding UTF8 -NoNewline
    return $ack
}

function Get-ContinuousCoReviewDegradedAck {
    param([Parameter(Mandatory)][string] $RepoRoot, [Parameter(Mandatory)][string] $RunId)
    $path = Join-Path (Resolve-Path -LiteralPath $RepoRoot).Path ".specrew/review/inline/$RunId/degraded-ack.json"
    if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { return $null }
    try { return (Get-Content -LiteralPath $path -Raw -Encoding UTF8 | ConvertFrom-Json) } catch { return $null }
}

function Test-ContinuousCoReviewOverrideAuthorization {
    # A well-formed override is an object carrying a non-empty authorized_by AND rationale.
    # Anything less is ignored (the gate proceeds normally) - an override is never implicit.
    #
    # F3/F4 (145 adversarial review) - TRUST BOUNDARY, bound to the deferred F-185 wiring:
    # this decision layer AUTHENTICATES nothing (it trusts the structural object) and
    # PERSISTS nothing. The wiring owner (the boundary-sync integration) MUST (1) construct
    # this object only from a genuinely human-authored authorization (e.g. the captured
    # verdict / Add-SpecrewBoundaryAuthorization), never from agent-forgeable input, and
    # (2) persist the returned decision (incl. the override) to durable gate-verdict evidence
    # so "RECORDED, never silent" holds. The same boundary applies to the review-run.json
    # records the chain walk trusts. A test MUST assert override persistence when wired.
    param([AllowNull()] $OverrideAuthorization)

    if ($null -eq $OverrideAuthorization) {
        return $false
    }

    $authorizedBy = [string] (Get-ContinuousCoReviewRunIndexProperty -Object $OverrideAuthorization -Name 'authorized_by')
    $rationale = [string] (Get-ContinuousCoReviewRunIndexProperty -Object $OverrideAuthorization -Name 'rationale')
    return (-not [string]::IsNullOrWhiteSpace($authorizedBy)) -and (-not [string]::IsNullOrWhiteSpace($rationale))
}

function Get-ContinuousCoReviewChainReachesAnchor {
    # Walk the chain from the digest-matched run back toward the anchor: each link is a
    # passing run whose reviewed_ref equals the current run's baseline_ref. The chain reaches
    # the anchor when a baseline is an ancestor-of-or-equal-to the anchor (so [anchor, HEAD]
    # is fully covered); a baseline that is neither the anchor-or-earlier nor a prior pass's
    # reviewed point is a GAP (un-reviewed span -> block).
    param(
        [Parameter(Mandatory)]
        [string] $RepoRoot,

        [Parameter(Mandatory)]
        [object[]] $PassingRuns,

        [Parameter(Mandatory)]
        $MatchedRun,

        [Parameter(Mandatory)]
        [string] $AnchorRef
    )

    $byReviewedRef = @{}
    foreach ($run in @($PassingRuns)) {
        $reviewedRef = [string] (Get-ContinuousCoReviewRunIndexProperty -Object $run -Name 'reviewed_ref')
        if (-not [string]::IsNullOrWhiteSpace($reviewedRef) -and -not $byReviewedRef.ContainsKey($reviewedRef)) {
            $byReviewedRef[$reviewedRef] = $run
        }
    }

    $current = $MatchedRun
    $visited = New-Object System.Collections.Generic.HashSet[string]
    for ($i = 0; $i -lt 4096; $i++) {
        $baseline = [string] (Get-ContinuousCoReviewRunIndexProperty -Object $current -Name 'baseline_ref')
        if ([string]::IsNullOrWhiteSpace($baseline)) {
            return [pscustomobject]@{ reached = $false; gap_at = [string] (Get-ContinuousCoReviewRunIndexProperty -Object $current -Name 'run_id') }
        }

        if (Get-ContinuousCoReviewGitIsAncestor -RepoRoot $RepoRoot -Ancestor $baseline -Descendant $AnchorRef) {
            return [pscustomobject]@{ reached = $true; gap_at = $null }
        }

        $runId = [string] (Get-ContinuousCoReviewRunIndexProperty -Object $current -Name 'run_id')
        if (-not $visited.Add($runId)) {
            return [pscustomobject]@{ reached = $false; gap_at = 'cycle' }
        }

        if (-not $byReviewedRef.ContainsKey($baseline)) {
            return [pscustomobject]@{ reached = $false; gap_at = $baseline }
        }

        $current = $byReviewedRef[$baseline]
    }

    return [pscustomobject]@{ reached = $false; gap_at = 'chain-too-long' }
}

function Test-ReviewCampaignFinalizationEnvelope {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$RepoRoot,
        [Parameter(Mandatory)][string]$CampaignId,
        [Parameter(Mandatory)]$Result,
        [Parameter(Mandatory)]$Fact,
        [Parameter(Mandatory)][string]$CurrentDigest,
        [AllowEmptyString()][string]$FeatureId,
        [AllowEmptyString()][string]$IterationNumber,
        [string[]]$ExcludedPathPatterns = @()
    )
    $fail = {
        param([string]$Reason)
        [pscustomobject][ordered]@{
            valid = $false; reason = $Reason; run_id = $null; reviewed_digest = $null
            reviewed_commit = $null; finalization_commit = $null; changed_paths = @()
        }
    }
    if (-not (Test-ReviewCampaignScopeIdentity -FeatureId $FeatureId -IterationNumber $IterationNumber)) {
        return & $fail 'scope-identity-invalid'
    }
    $factValidation = Test-ReviewAuthorityContractObject -ContractName ReviewFinalizationFact -InputObject $Fact -ExpectedCampaignId $CampaignId
    if (-not $factValidation.valid) { return & $fail ('fact-' + $factValidation.category) }
    $runId = [string]$Fact.run_id
    $reviewedDigest = [string]$Fact.reviewed_digest
    $finalizationCommit = [string]$Fact.finalization_commit
    $resultValidation = Test-ReviewAuthorityContractObject -ContractName ReviewResult -InputObject $Result `
        -ExpectedCampaignId $CampaignId -ExpectedRunId $runId -ExpectedTargetDigest $reviewedDigest
    if (-not $resultValidation.valid) { return & $fail ('result-' + $resultValidation.category) }
    if ([string]$Result.completion -cne 'complete' -or [string]$Result.verdict -cne 'pass' -or
        [string]$Result.runtime_outcome -cne 'completed' -or -not [bool]$Result.termination_verified -or
        [string]$Result.containment -cne 'verified' -or [string]$Result.currentness -cne 'current' -or
        [string]$Result.validation -cne 'valid' -or -not [bool]$Result.can_approve_current) {
        return & $fail 'result-not-clean-current-pass'
    }

    $root = (Resolve-Path -LiteralPath $RepoRoot).Path
    $head = [string](@(& git -C $root rev-parse --verify 'HEAD^{commit}' 2>$null) | Select-Object -First 1)
    if ($LASTEXITCODE -ne 0 -or $head.Trim() -cne $finalizationCommit) { return & $fail 'finalization-not-current-head' }
    $parentLine = [string](@(& git -C $root rev-list --parents -n 1 $finalizationCommit 2>$null) | Select-Object -First 1)
    if ($LASTEXITCODE -ne 0) { return & $fail 'finalization-commit-unresolvable' }
    $parentParts = @($parentLine.Trim().Split(' ', [StringSplitOptions]::RemoveEmptyEntries))
    if ($parentParts.Count -ne 2) { return & $fail 'finalization-parent-not-singular' }
    $reviewedCommit = [string]$parentParts[1]
    & git -C $root cat-file -e "$reviewedDigest^{tree}" 2>$null
    if ($LASTEXITCODE -ne 0) { return & $fail 'reviewed-digest-unresolvable' }

    if (-not (Get-Command -Name 'Get-ContinuousCoReviewMachineryPaths' -ErrorAction SilentlyContinue)) {
        $worktreeReviewerPath = Join-Path $PSScriptRoot 'worktree-reviewer.ps1'
        if (Test-Path -LiteralPath $worktreeReviewerPath -PathType Leaf) {
            try { . $worktreeReviewerPath } catch { $null = $_ }
        }
    }
    if (-not (Get-Command -Name 'Get-ContinuousCoReviewDigestRuntimeStripList' -ErrorAction SilentlyContinue) -or
        -not (Get-Command -Name 'Get-ContinuousCoReviewMachineryPaths' -ErrorAction SilentlyContinue) -or
        -not (Get-Command -Name 'Test-ContinuousCoReviewDigestPathDenied' -ErrorAction SilentlyContinue)) {
        return & $fail 'digest-policy-unavailable'
    }
    $stripPatterns = @(Get-ContinuousCoReviewDigestRuntimeStripList) + @($ExcludedPathPatterns)
    try {
        foreach ($machineryPath in @(Get-ContinuousCoReviewMachineryPaths -RepoRoot $root)) {
            if ([string]::IsNullOrWhiteSpace([string]$machineryPath)) { continue }
            $stripPatterns += [string]$machineryPath
            $stripPatterns += ('{0}/**' -f [string]$machineryPath)
        }
    }
    catch { return & $fail 'digest-policy-unavailable' }

    foreach ($comparison in @(
        @{ left = $reviewedDigest; right = $reviewedCommit; reason = 'reviewed-digest-not-parent-state' },
        @{ left = $CurrentDigest; right = $finalizationCommit; reason = 'current-state-not-finalization-commit' }
    )) {
        $differences = @(& git -C $root -c core.quotepath=false diff --name-only --no-renames ([string]$comparison.left) ([string]$comparison.right) 2>$null)
        if ($LASTEXITCODE -ne 0) { return & $fail ([string]$comparison.reason) }
        foreach ($path in $differences) {
            if (-not (Test-ContinuousCoReviewDigestPathDenied -Path ([string]$path).Replace('\', '/') -Denylist $stripPatterns)) {
                return & $fail ([string]$comparison.reason)
            }
        }
    }

    $iterationRoot = "specs/$FeatureId/iterations/$IterationNumber"
    $allowed = [Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal)
    foreach ($name in @('review.md', 'reviewer-index.md', 'code-map.md', 'coverage-evidence.md', 'dependency-report.md', 'review-diagrams.md')) {
        $null = $allowed.Add("$iterationRoot/$name")
    }
    $changedPaths = [Collections.Generic.List[string]]::new()
    $entries = @(& git -C $root -c core.quotepath=false diff-tree --no-commit-id --name-status --no-renames -r $reviewedCommit $finalizationCommit 2>$null)
    if ($LASTEXITCODE -ne 0 -or $entries.Count -eq 0) { return & $fail 'finalization-diff-empty-or-unresolvable' }
    foreach ($entry in $entries) {
        if ([string]$entry -cnotmatch '^(?<status>[AM])\t(?<path>.+)$') { return & $fail 'finalization-diff-status-denied' }
        $path = [string]$Matches.path
        if (-not $allowed.Contains($path)) { return & $fail ('finalization-path-denied:' + $path) }
        $changedPaths.Add($path) | Out-Null
    }
    return [pscustomobject][ordered]@{
        valid = $true; reason = 'valid'; run_id = $runId; reviewed_digest = $reviewedDigest
        reviewed_commit = $reviewedCommit; finalization_commit = $finalizationCommit; changed_paths = @($changedPaths)
    }
}

function New-ReviewCampaignVerdictPacketDecision {
    param(
        [Parameter(Mandatory)][string]$Route,
        [Parameter(Mandatory)][string]$Reason,
        [Parameter(Mandatory)][string]$Message,
        [string]$CampaignId,
        [string]$RunId,
        [string]$TargetDigest,
        [string]$ReviewedDigest,
        [string]$ReviewedCommit,
        [string]$FinalizationCommit,
        [bool]$RenderBoundaryPacket = $false,
        [bool]$AskNarrowQuestion = $false,
        [string]$ImplementerAction = 'wait'
    )
    return [pscustomobject][ordered]@{
        schema_version = '1.0'; route = $Route; reason = $Reason; message = $Message
        campaign_id = $CampaignId; run_id = $RunId; target_digest = $TargetDigest
        reviewed_digest = $ReviewedDigest; reviewed_commit = $ReviewedCommit; finalization_commit = $FinalizationCommit
        render_boundary_packet = $RenderBoundaryPacket; render_verdict_marker = $RenderBoundaryPacket
        ask_narrow_question = $AskNarrowQuestion; implementer_action = $ImplementerAction
    }
}

function Resolve-ReviewCampaignVerdictPacketDecision {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$CampaignId,
        [Parameter(Mandatory)][string]$CurrentDigest,
        [AllowEmptyCollection()][string[]]$OrderedRunIds = @(),
        [AllowEmptyCollection()][object[]]$Results = @(),
        [AllowNull()]$ActiveRun,
        [AllowEmptyCollection()][object[]]$HumanDispositions = @()
    )
    if (-not (Test-ReviewAuthorityIdentifier -Value $CampaignId -Kind campaign) -or [string]::IsNullOrWhiteSpace($CurrentDigest)) {
        return New-ReviewCampaignVerdictPacketDecision -Route 'review-failure' -Reason 'campaign-or-digest-invalid' -Message 'Campaign identity or current digest is unavailable; no lifecycle verdict may be requested.' -CampaignId $CampaignId -TargetDigest $CurrentDigest -ImplementerAction 'repair-review-state'
    }
    $byRun = @{}
    foreach ($result in @($Results)) {
        $validation = Test-ReviewAuthorityContractObject -ContractName ReviewResult -InputObject $result -ExpectedCampaignId $CampaignId
        if (-not $validation.valid) {
            return New-ReviewCampaignVerdictPacketDecision -Route 'review-failure' -Reason ('campaign-result-invalid:' + $validation.category) -Message 'Campaign result authority is malformed or identity-mismatched; no lifecycle verdict may be requested.' -CampaignId $CampaignId -TargetDigest $CurrentDigest -ImplementerAction 'repair-review-state'
        }
        $runId = [string]$result.run_id
        if ($byRun.ContainsKey($runId)) {
            return New-ReviewCampaignVerdictPacketDecision -Route 'review-failure' -Reason 'duplicate-terminal-result-for-run' -Message 'Conflicting terminal results exist for one run; review authority fails closed.' -CampaignId $CampaignId -RunId $runId -TargetDigest $CurrentDigest -ImplementerAction 'repair-review-state'
        }
        $byRun[$runId] = $result
    }
    $ordered = [Collections.Generic.List[string]]::new()
    foreach ($runId in @($OrderedRunIds)) {
        if (-not (Test-ReviewAuthorityIdentifier -Value $runId -Kind run) -or $ordered.Contains($runId)) {
            return New-ReviewCampaignVerdictPacketDecision -Route 'review-failure' -Reason 'campaign-run-order-invalid' -Message 'Campaign run order is malformed or ambiguous; review authority fails closed.' -CampaignId $CampaignId -TargetDigest $CurrentDigest -ImplementerAction 'repair-review-state'
        }
        $ordered.Add($runId) | Out-Null
    }

    if ($null -ne $ActiveRun) {
        $activeValidation = Test-ReviewAuthorityContractObject -ContractName ReviewRun -InputObject $ActiveRun -ExpectedCampaignId $CampaignId
        if (-not $activeValidation.valid) {
            return New-ReviewCampaignVerdictPacketDecision -Route 'review-failure' -Reason 'active-run-invalid' -Message 'The active campaign run is malformed; review authority fails closed.' -CampaignId $CampaignId -TargetDigest $CurrentDigest -ImplementerAction 'repair-review-state'
        }
        $activeRunId = [string]$ActiveRun.run_id
        if ($byRun.ContainsKey($activeRunId)) {
            return New-ReviewCampaignVerdictPacketDecision -Route 'review-failure' -Reason 'terminal-result-still-has-active-claim' -Message 'A terminal result exists while its run claim is still active; reconciliation must retire the claim before signoff.' -CampaignId $CampaignId -RunId $activeRunId -TargetDigest $CurrentDigest -ImplementerAction 'reconcile-run-claim'
        }
        if ([string]$ActiveRun.target_digest -ceq $CurrentDigest) {
            return New-ReviewCampaignVerdictPacketDecision -Route 'review-running' -Reason 'current-review-in-flight' -Message 'The single campaign review for the current digest is still running; no human decision is required.' -CampaignId $CampaignId -RunId $activeRunId -TargetDigest $CurrentDigest -ImplementerAction 'poll-existing-run'
        }
        return New-ReviewCampaignVerdictPacketDecision -Route 'review-stale' -Reason 'in-flight-review-target-moved' -Message 'The active review targets an earlier digest and cannot authorize the current tree.' -CampaignId $CampaignId -RunId $activeRunId -TargetDigest $CurrentDigest -ImplementerAction 'complete-or-reconcile-then-rerun-current'
    }

    if ($ordered.Count -eq 0) {
        return New-ReviewCampaignVerdictPacketDecision -Route 'review-required' -Reason 'no-authoritative-campaign-result' -Message 'No claim-ordered campaign result can authorize the current digest.' -CampaignId $CampaignId -TargetDigest $CurrentDigest -ImplementerAction 'request-authorized-review'
    }

    # A newer claimed invocation supersedes every older result, including an older clean result.
    # Otherwise a final timed-out/partial review (for example T061's signoff harness) could silently
    # fall back to an earlier pass. A claimed run without its terminal result is recovery work, not
    # permission to select around the gap.
    $latestRunId = $ordered[$ordered.Count - 1]
    if (-not $byRun.ContainsKey($latestRunId)) {
        return New-ReviewCampaignVerdictPacketDecision -Route 'review-failure' -Reason 'latest-claimed-run-missing-result' -Message 'The latest claimed campaign run has no terminal result; reconciliation must close the gap before signoff.' -CampaignId $CampaignId -RunId $latestRunId -TargetDigest $CurrentDigest -ImplementerAction 'reconcile-run-claim'
    }
    $latest = $byRun[$latestRunId]
    if ([string]$latest.target_digest -cne $CurrentDigest -or [string]$latest.currentness -ceq 'snapshot-moved') {
        return New-ReviewCampaignVerdictPacketDecision -Route 'review-stale' -Reason 'latest-result-not-current' -Message 'The latest campaign result remains useful evidence but targets a moved or earlier snapshot and cannot authorize the current tree.' -CampaignId $CampaignId -RunId $latestRunId -TargetDigest $CurrentDigest -ImplementerAction 'request-current-digest-review'
    }
    if ([string]$latest.runtime_outcome -ceq 'timed-out') {
        return New-ReviewCampaignVerdictPacketDecision -Route 'review-timeout' -Reason 'latest-review-timed-out' -Message ('The review timed out: ' + [string]$latest.failure_reason) -CampaignId $CampaignId -RunId $latestRunId -TargetDigest $CurrentDigest -ImplementerAction 'report-failure-and-request-rerun-grant'
    }
    if ([string]$latest.completion -cne 'complete') {
        return New-ReviewCampaignVerdictPacketDecision -Route 'review-partial' -Reason 'latest-review-incomplete' -Message 'Validated partial findings remain advisory, but a complete separately authorized run is required.' -CampaignId $CampaignId -RunId $latestRunId -TargetDigest $CurrentDigest -ImplementerAction 'use-partial-findings-and-request-rerun-grant'
    }
    if ([string]$latest.validation -cne 'valid' -or [string]$latest.currentness -cne 'current') {
        return New-ReviewCampaignVerdictPacketDecision -Route 'review-failure' -Reason ('latest-review-' + [string]$latest.runtime_outcome) -Message ('The campaign review failed: ' + [string]$latest.failure_reason) -CampaignId $CampaignId -RunId $latestRunId -TargetDigest $CurrentDigest -ImplementerAction 'report-failure-and-request-rerun-grant'
    }
    if ([bool]$latest.can_approve_current -and [string]$latest.verdict -ceq 'pass') {
        return New-ReviewCampaignVerdictPacketDecision -Route 'boundary-clean' -Reason 'complete-current-clean-result' -Message 'The authoritative campaign result is a complete valid pass for the exact current digest.' -CampaignId $CampaignId -RunId $latestRunId -TargetDigest $CurrentDigest -RenderBoundaryPacket $true -ImplementerAction 'render-boundary-packet'
    }
    if ([string]$latest.verdict -ceq 'findings') {
        $matchingDispositions = @($HumanDispositions | Where-Object {
            $v = Test-ReviewAuthorityContractObject -ContractName HumanDispositionFact -InputObject $_ -ExpectedCampaignId $CampaignId -ExpectedRunId $latestRunId -ExpectedTargetDigest $CurrentDigest
            $v.valid
        })
        $requiresCorrection = @($matchingDispositions | Where-Object { [string]$_.decision -ceq 'require-correction' }).Count -gt 0
        $accepted = @($matchingDispositions | Where-Object { [string]$_.decision -ceq 'accept-current' }).Count -gt 0
        if ($accepted -and -not $requiresCorrection) {
            return New-ReviewCampaignVerdictPacketDecision -Route 'boundary-human-disposition' -Reason 'complete-current-findings-human-accepted' -Message 'The exact current result has an explicit identity-bound human disposition accepting its findings.' -CampaignId $CampaignId -RunId $latestRunId -TargetDigest $CurrentDigest -RenderBoundaryPacket $true -ImplementerAction 'render-boundary-packet'
        }
        $actionable = @($latest.findings | Where-Object { [string]$_.resolution -ceq 'open' -and [string]$_.severity -in @('blocking', 'major') }).Count -gt 0
        if ($actionable -or $requiresCorrection) {
            return New-ReviewCampaignVerdictPacketDecision -Route 'review-actionable' -Reason 'complete-current-actionable-findings' -Message 'The exact current review has actionable findings; suppress the boundary packet, correct them, and run a separately authorized review.' -CampaignId $CampaignId -RunId $latestRunId -TargetDigest $CurrentDigest -ImplementerAction 'fix-and-request-rerun-grant'
        }
        return New-ReviewCampaignVerdictPacketDecision -Route 'review-human-decision' -Reason 'complete-current-advisory-findings' -Message 'The exact current review has advisory findings that require a narrow human disposition before any boundary packet.' -CampaignId $CampaignId -RunId $latestRunId -TargetDigest $CurrentDigest -AskNarrowQuestion $true -ImplementerAction 'ask-narrow-non-boundary-question'
    }
    return New-ReviewCampaignVerdictPacketDecision -Route 'review-failure' -Reason ('latest-review-' + [string]$latest.runtime_outcome) -Message ('The campaign review failed: ' + [string]$latest.failure_reason) -CampaignId $CampaignId -RunId $latestRunId -TargetDigest $CurrentDigest -ImplementerAction 'report-failure-and-request-rerun-grant'
}

function Get-ReviewCampaignVerdictPacketDecision {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$RepoRoot,
        [string]$CampaignId,
        [string]$TargetLineage,
        [string]$StoreRoot,
        [string]$FeatureId,
        [string]$IterationNumber,
        [string[]]$ExcludedPathPatterns = @()
    )
    $root = (Resolve-Path -LiteralPath $RepoRoot).Path
    $identity = $null
    if ([string]::IsNullOrWhiteSpace($CampaignId) -or [string]::IsNullOrWhiteSpace($TargetLineage)) {
        $identity = Resolve-ReviewCampaignPublicIdentity -RepoRoot $root -FeatureId $FeatureId -IterationNumber $IterationNumber -RunId 'run-gate-probe'
        if ([string]::IsNullOrWhiteSpace($CampaignId)) { $CampaignId = [string]$identity.campaign_id }
        if ([string]::IsNullOrWhiteSpace($TargetLineage)) { $TargetLineage = [string]$identity.target_lineage }
        if ([string]::IsNullOrWhiteSpace($FeatureId)) { $FeatureId = [string]$identity.feature_id }
        if ([string]::IsNullOrWhiteSpace($IterationNumber)) { $IterationNumber = [string]$identity.iteration_number }
    }
    if ($null -ne $identity -and ([string]::IsNullOrWhiteSpace($CampaignId) -or [string]::IsNullOrWhiteSpace($TargetLineage) -or
        -not (Test-ReviewCampaignScopeIdentity -FeatureId $FeatureId -IterationNumber $IterationNumber))) {
        return New-ReviewCampaignVerdictPacketDecision -Route 'review-failure' -Reason 'scope-identity-unresolvable' `
            -Message 'Campaign, lineage, feature, and iteration identity must resolve before finalization validation or publication.' `
            -CampaignId $CampaignId -ImplementerAction 'repair-review-state'
    }
    if ([string]::IsNullOrWhiteSpace($StoreRoot)) { $StoreRoot = Join-Path $root '.specrew/review/authority' }
    $digest = Get-ContinuousCoReviewReviewedStateDigest -RepoRoot $root -ExcludedPathPatterns $ExcludedPathPatterns
    if ($null -eq $digest -or -not $digest.ok -or [string]::IsNullOrWhiteSpace([string]$digest.tree_id)) {
        return New-ReviewCampaignVerdictPacketDecision -Route 'review-failure' -Reason 'digest-unresolvable' -Message 'The current reviewed-state digest could not be computed; no lifecycle verdict may be requested.' -CampaignId $CampaignId -ImplementerAction 'repair-review-state'
    }
    $claimFacts = @(Get-ReviewAuthorityClaimFacts -StoreRoot $StoreRoot -CampaignId $CampaignId -TargetLineage $TargetLineage)
    $orderedRunIds = @($claimFacts | Where-Object { [string]$_.fact_type -ceq 'claim-held' } | Sort-Object { [int]$_.generation } | ForEach-Object { [string]$_.run_id })
    $activeClaim = Get-ReviewAuthorityActiveClaim -Facts $claimFacts
    $activeRun = if ($null -ne $activeClaim) { Get-ReviewRunLatestStateFact -StoreRoot $StoreRoot -CampaignId $CampaignId -RunId ([string]$activeClaim.run_id) } else { $null }
    if ($null -ne $activeClaim -and $null -eq $activeRun) {
        return New-ReviewCampaignVerdictPacketDecision -Route 'review-failure' -Reason 'active-claim-run-state-missing' -Message 'An active campaign claim has no readable run state; reconciliation must repair the authority gap before signoff.' -CampaignId $CampaignId -RunId ([string]$activeClaim.run_id) -TargetDigest ([string]$digest.tree_id) -ImplementerAction 'reconcile-run-claim'
    }
    $results = @(Get-ReviewAuthorityCampaignRunResults -StoreRoot $StoreRoot -CampaignId $CampaignId)
    $dispositions = @(Get-ReviewCampaignHumanDispositionFacts -StoreRoot $StoreRoot -CampaignId $CampaignId)
    $finalizationFact = Get-ReviewCampaignFinalizationFact -StoreRoot $StoreRoot -CampaignId $CampaignId
    $finalizationEnvelope = $null
    $latestResult = $null
    if ($orderedRunIds.Count -gt 0) {
        $latestRunId = [string]$orderedRunIds[$orderedRunIds.Count - 1]
        $latestResult = @($results | Where-Object { [string]$_.run_id -ceq $latestRunId } | Select-Object -First 1)
        if ($latestResult.Count -gt 0) { $latestResult = $latestResult[0] } else { $latestResult = $null }
    }
    $finalizationCandidateEligible = $null -eq $activeRun -and $null -ne $latestResult -and
        [string]$latestResult.target_digest -cne [string]$digest.tree_id -and
        [string]$latestResult.completion -ceq 'complete' -and [string]$latestResult.verdict -ceq 'pass' -and
        [string]$latestResult.runtime_outcome -ceq 'completed' -and [bool]$latestResult.can_approve_current
    if (($null -ne $finalizationFact -or $finalizationCandidateEligible) -and
        -not (Test-ReviewCampaignScopeIdentity -FeatureId $FeatureId -IterationNumber $IterationNumber)) {
        return New-ReviewCampaignVerdictPacketDecision -Route 'review-failure' -Reason 'scope-identity-unresolvable' `
            -Message 'Feature and iteration identity must resolve before finalization validation or publication.' `
            -CampaignId $CampaignId -RunId $(if ($null -ne $latestResult) { [string]$latestResult.run_id } else { $null }) `
            -TargetDigest ([string]$digest.tree_id) -ImplementerAction 'repair-review-state'
    }
    if ($null -ne $finalizationFact) {
        if ($null -eq $latestResult) {
            return New-ReviewCampaignVerdictPacketDecision -Route 'review-failure' -Reason 'review-finalization-result-missing' -Message 'The one-time finalization fact has no matching latest campaign result; authority fails closed.' -CampaignId $CampaignId -TargetDigest ([string]$digest.tree_id) -ImplementerAction 'repair-review-state'
        }
        $finalizationEnvelope = Test-ReviewCampaignFinalizationEnvelope -RepoRoot $root -CampaignId $CampaignId -Result $latestResult `
            -Fact $finalizationFact -CurrentDigest ([string]$digest.tree_id) -FeatureId $FeatureId -IterationNumber $IterationNumber `
            -ExcludedPathPatterns $ExcludedPathPatterns
        if (-not $finalizationEnvelope.valid) {
            return New-ReviewCampaignVerdictPacketDecision -Route 'review-failure' -Reason ('review-finalization-invalid:' + [string]$finalizationEnvelope.reason) -Message 'The one-time review finalization fact or its commit envelope is invalid; authority fails closed.' -CampaignId $CampaignId -RunId ([string]$finalizationFact.run_id) -TargetDigest ([string]$digest.tree_id) -ImplementerAction 'repair-review-state'
        }
    }
    elseif ($finalizationCandidateEligible) {
        $candidateFact = [pscustomobject][ordered]@{
            schema_version = '1.0'; fact_type = 'review-finalization'; campaign_id = $CampaignId
            run_id = [string]$latestResult.run_id; reviewed_digest = [string]$latestResult.target_digest
            finalization_commit = [string](@(& git -C $root rev-parse --verify 'HEAD^{commit}' 2>$null) | Select-Object -First 1)
        }
        $candidateEnvelope = Test-ReviewCampaignFinalizationEnvelope -RepoRoot $root -CampaignId $CampaignId -Result $latestResult `
            -Fact $candidateFact -CurrentDigest ([string]$digest.tree_id) -FeatureId $FeatureId -IterationNumber $IterationNumber `
            -ExcludedPathPatterns $ExcludedPathPatterns
        if ($candidateEnvelope.valid) {
            try {
                Write-ReviewCampaignFinalizationFact -StoreRoot $StoreRoot -Fact $candidateFact | Out-Null
            }
            catch {
                if ($_.Exception.Message -notlike 'review-store-corruption:conflicting-immutable-fact:*') { throw }
                # Another gate may have won CreateNew after this gate validated its candidate. The
                # winner is authoritative only if the normal read + envelope validation below proves
                # it binds this same clean result and current finalization commit.
            }
            $finalizationFact = Get-ReviewCampaignFinalizationFact -StoreRoot $StoreRoot -CampaignId $CampaignId
            if ($null -eq $finalizationFact) { throw 'review-finalization-post-publish-missing' }
            $finalizationEnvelope = Test-ReviewCampaignFinalizationEnvelope -RepoRoot $root -CampaignId $CampaignId -Result $latestResult `
                -Fact $finalizationFact -CurrentDigest ([string]$digest.tree_id) -FeatureId $FeatureId -IterationNumber $IterationNumber `
                -ExcludedPathPatterns $ExcludedPathPatterns
            if (-not $finalizationEnvelope.valid) { throw ('review-finalization-post-publish-invalid:' + [string]$finalizationEnvelope.reason) }
        }
    }
    $decisionDigest = if ($null -ne $finalizationEnvelope) { [string]$finalizationEnvelope.reviewed_digest } else { [string]$digest.tree_id }
    $packet = Resolve-ReviewCampaignVerdictPacketDecision -CampaignId $CampaignId -CurrentDigest $decisionDigest -OrderedRunIds $orderedRunIds -Results $results -ActiveRun $activeRun -HumanDispositions $dispositions
    if ($null -eq $finalizationEnvelope) { return $packet }
    if ([string]$packet.route -cne 'boundary-clean') {
        return New-ReviewCampaignVerdictPacketDecision -Route 'review-failure' -Reason 'review-finalization-result-not-clean' -Message 'The finalization envelope is valid but its bound result is not an authorizing clean result; authority fails closed.' -CampaignId $CampaignId -RunId ([string]$finalizationEnvelope.run_id) -TargetDigest ([string]$digest.tree_id) -ImplementerAction 'repair-review-state'
    }
    $message = 'The authoritative campaign result reviewed commit {0} at digest {1}; the controller finalized its allowlisted review evidence exactly once as commit {2}.' -f `
        [string]$finalizationEnvelope.reviewed_commit, [string]$finalizationEnvelope.reviewed_digest, [string]$finalizationEnvelope.finalization_commit
    return New-ReviewCampaignVerdictPacketDecision -Route 'boundary-finalized' -Reason 'complete-clean-finalized-result' -Message $message `
        -CampaignId $CampaignId -RunId ([string]$finalizationEnvelope.run_id) -TargetDigest ([string]$digest.tree_id) `
        -ReviewedDigest ([string]$finalizationEnvelope.reviewed_digest) -ReviewedCommit ([string]$finalizationEnvelope.reviewed_commit) `
        -FinalizationCommit ([string]$finalizationEnvelope.finalization_commit) -RenderBoundaryPacket $true -ImplementerAction 'render-boundary-packet'
}

function Get-ContinuousCoReviewSignoffGateDecision {
    param(
        [Parameter(Mandatory)]
        [string] $RepoRoot,

        [AllowEmptyString()][string] $TrunkName = '',

        [string[]] $ExcludedPathPatterns = @(),

        [AllowNull()] $OverrideAuthorization,

        # T094/FR-036: an explicit degraded-evidence acknowledgement (authorized_by + rationale).
        # When omitted, the persisted per-run ack (degraded-ack.json) is honoured instead.
        [AllowNull()] $DegradedAcknowledgement,

        [string] $AuthorityConfigPath,
        [string] $CampaignId,
        [string] $TargetLineage,
        [string] $FeatureId,
        [string] $IterationNumber,
        [string] $CampaignStoreRoot
    )

    $resolvedRepoRoot = (Resolve-Path -LiteralPath $RepoRoot).Path

    $authority = if (Get-Command -Name 'Get-ContinuousCoReviewAuthorityDecision' -ErrorAction SilentlyContinue) {
        Get-ContinuousCoReviewAuthorityDecision -ConfigPath $AuthorityConfigPath
    }
    else { [pscustomobject]@{ mode = 'disabled'; valid = $false; legacy_promotion_enabled = $false; campaign_authority_enabled = $false; reason = 'authority-cutover-helper-missing' } }
    if (-not $authority.valid -or [string]$authority.mode -ceq 'disabled') {
        return New-ContinuousCoReviewSignoffGateDecision -Decision 'block' -Reason ('review-authority-disabled:' + [string]$authority.reason) -Message 'Review authority is missing, malformed, or disabled; neither legacy nor campaign evidence may authorize signoff.'
    }
    if ([bool]$authority.campaign_authority_enabled) {
        try {
            $packet = Get-ReviewCampaignVerdictPacketDecision -RepoRoot $resolvedRepoRoot -CampaignId $CampaignId -TargetLineage $TargetLineage -StoreRoot $CampaignStoreRoot -FeatureId $FeatureId -IterationNumber $IterationNumber -ExcludedPathPatterns $ExcludedPathPatterns
        }
        catch {
            return New-ContinuousCoReviewSignoffGateDecision -Decision 'block' -Reason 'campaign-review-state-invalid' -Message ('Campaign review authority could not be read safely: ' + $_.Exception.Message)
        }
        $decision = New-ContinuousCoReviewSignoffGateDecision -Decision $(if ($packet.render_boundary_packet) { 'allow' } else { 'block' }) -Reason $packet.reason -Message $packet.message -CurrentTreeId $packet.target_digest -MatchedRunId $packet.run_id
        foreach ($property in @('route', 'campaign_id', 'render_boundary_packet', 'render_verdict_marker', 'ask_narrow_question', 'implementer_action', 'reviewed_digest', 'reviewed_commit', 'finalization_commit')) {
            $decision | Add-Member -NotePropertyName $property -NotePropertyValue $packet.$property
        }
        return $decision
    }

    # A well-formed human-authorized recorded override short-circuits, with the authorization
    # captured in the decision evidence (auditable, never silent).
    if (Test-ContinuousCoReviewOverrideAuthorization -OverrideAuthorization $OverrideAuthorization) {
        return New-ContinuousCoReviewSignoffGateDecision -Decision 'allow' -Reason 'human-authorized-partial-override' -Message 'Signoff allowed under a recorded human-authorized partial-coverage override.' -OverrideAuthorization $OverrideAuthorization
    }

    # 1. Current reviewed-state digest (fail-closed on any digest/git failure).
    $digest = Get-ContinuousCoReviewReviewedStateDigest -RepoRoot $resolvedRepoRoot -ExcludedPathPatterns $ExcludedPathPatterns
    if (-not $digest.ok) {
        return New-ContinuousCoReviewSignoffGateDecision -Decision 'block' -Reason 'digest-unresolvable' -Message "The current reviewed-state digest could not be computed ($($digest.failure_reason)); treat as unsafe."
    }
    if ($digest.is_empty) {
        return New-ContinuousCoReviewSignoffGateDecision -Decision 'block' -Reason 'empty-reviewed-state' -Message 'The current reviewable working tree is empty; there is no reviewed content to sign off on.' -CurrentTreeId $digest.tree_id
    }

    # 2. Trusted anchor = merge-base with the trunk (fail-closed if it cannot be resolved).
    $anchor = Get-ContinuousCoReviewMergeBaseAnchor -RepoRoot $resolvedRepoRoot -TrunkName $TrunkName
    if ([string]::IsNullOrWhiteSpace($anchor)) {
        return New-ContinuousCoReviewSignoffGateDecision -Decision 'block' -Reason 'anchor-unresolvable' -Message "The trusted anchor (merge-base with '$TrunkName') could not be resolved; coverage cannot be verified." -CurrentTreeId $digest.tree_id
    }

    # 3. Lineage-valid passing runs.
    $passingRuns = @(Get-ContinuousCoReviewPassingReviewRuns -RepoRoot $resolvedRepoRoot -AncestorOfRef 'HEAD')
    if ($passingRuns.Count -eq 0) {
        return New-ContinuousCoReviewSignoffGateDecision -Decision 'block' -Reason 'no-co-review-evidence' -Message 'No passing or escalated co-review run on this lineage; the current state has not been co-reviewed.' -CurrentTreeId $digest.tree_id -AnchorRef $anchor
    }

    # 4. Freshness: a passing run whose recorded reviewed_tree_id equals the current digest.
    $matched = $null
    $emptyTreeId = Get-ContinuousCoReviewEmptyTreeId
    foreach ($run in $passingRuns) {
        $recordedTreeId = [string] (Get-ContinuousCoReviewRunIndexProperty -Object $run -Name 'reviewed_tree_id')
        if ([string]::IsNullOrWhiteSpace($recordedTreeId) -or $recordedTreeId -eq $emptyTreeId) {
            continue
        }
        if ($recordedTreeId -eq $digest.tree_id) {
            $matched = $run
            break
        }
    }
    # 4b. F-198 FR-020 (mechanism b): the ANNOUNCED tracker-only bypass. When no run matches
    # exactly, a passing run whose ONLY delta to the current tree is machine-managed tracker
    # bookkeeping - with claims verified as a subset of the review record that run already
    # accepted - keeps its evidence fresh. Fail-closed: any parse ambiguity or claim increase
    # falls through to the stale block exactly as before. The digest formula is untouched.
    $honestyBypassNote = $null
    $dishonestReason = $null
    if ($null -eq $matched -and (Get-Command -Name 'Get-ContinuousCoReviewTrackerOnlyDelta' -ErrorAction SilentlyContinue)) {
        foreach ($run in $passingRuns) {
            $recordedTreeId = [string] (Get-ContinuousCoReviewRunIndexProperty -Object $run -Name 'reviewed_tree_id')
            if ([string]::IsNullOrWhiteSpace($recordedTreeId) -or $recordedTreeId -eq $emptyTreeId) { continue }
            $delta = Get-ContinuousCoReviewTrackerOnlyDelta -RepoRoot $resolvedRepoRoot -FromTreeId $recordedTreeId -ToTreeId $digest.tree_id
            if (-not $delta.Ok -or -not $delta.TrackerOnly) { continue }
            $honesty = Test-ContinuousCoReviewTrackerReconcileHonest -RepoRoot $resolvedRepoRoot -FromTreeId $recordedTreeId -ToTreeId $digest.tree_id -TrackerPaths @($delta.Paths)
            if ($honesty.Honest) {
                $matched = $run
                $honestyBypassNote = ("TRACKER-ONLY RECONCILE ACCEPTED: the only change since the reviewed tree is tracker bookkeeping ({0}) whose claims match the already-accepted review record; that run's evidence is kept fresh. " -f (@($delta.Paths) -join ', '))
                break
            }
            $dishonestReason = $honesty.Reason
        }
    }
    if ($null -eq $matched) {
        $staleMessage = 'The current working tree does not match any passing co-review; re-run continuous co-review before signoff.'
        if (-not [string]::IsNullOrWhiteSpace($dishonestReason)) {
            $staleMessage = ("The current working tree does not match any passing co-review, and the tracker-only change could not be accepted ({0}) - a claims-increasing tracker edit needs a fresh review, exactly as any content change." -f $dishonestReason)
        }
        return New-ContinuousCoReviewSignoffGateDecision -Decision 'block' -Reason 'stale-co-review-evidence' -Message $staleMessage -CurrentTreeId $digest.tree_id -AnchorRef $anchor
    }

    # 5. Coverage: the matched run's chain must reach the anchor with no gap.
    $chain = Get-ContinuousCoReviewChainReachesAnchor -RepoRoot $resolvedRepoRoot -PassingRuns $passingRuns -MatchedRun $matched -AnchorRef $anchor
    if (-not $chain.reached) {
        return New-ContinuousCoReviewSignoffGateDecision -Decision 'block' -Reason 'coverage-gap' -Message "The reviewed chain does not reach the trunk anchor (gap at $($chain.gap_at)); some feature content was never co-reviewed." -CurrentTreeId $digest.tree_id -MatchedRunId ([string] (Get-ContinuousCoReviewRunIndexProperty -Object $matched -Name 'run_id')) -AnchorRef $anchor
    }

    # 6. T094/FR-036 (iter-009 D4) - the TIERED assurance decision on the matched evidence:
    # full + independent (any budget: 'time-extended' is NOT reduced assurance) -> auto-allow;
    # partial OR not-provably-independent -> allow ONLY with a recorded first-class human ack.
    # NEVER deadlocks: the worst case is the ack ask below, always satisfiable via
    # `specrew review --ack-degraded <run-id> --ack-reason "<why>"`.
    $matchedRunId = [string] (Get-ContinuousCoReviewRunIndexProperty -Object $matched -Name 'run_id')
    $labels = Get-ContinuousCoReviewRunEvidenceLabels -Run $matched
    if (-not (Test-ContinuousCoReviewEvidenceIsDegraded -Labels $labels)) {
        return New-ContinuousCoReviewSignoffGateDecision -Decision 'allow' -Reason 'fresh-and-covered' -Message ("{0}The current reviewed-state matches a passing co-review whose chain covers the feature back to the trunk anchor." -f [string]$honestyBypassNote) -CurrentTreeId $digest.tree_id -MatchedRunId $matchedRunId -AnchorRef $anchor -EvidenceLabels $labels
    }

    $ack = $DegradedAcknowledgement
    if (-not (Test-ContinuousCoReviewOverrideAuthorization -OverrideAuthorization $ack)) {
        $ack = Get-ContinuousCoReviewDegradedAck -RepoRoot $resolvedRepoRoot -RunId $matchedRunId
    }
    if (Test-ContinuousCoReviewOverrideAuthorization -OverrideAuthorization $ack) {
        # FR-020 ANNOUNCED (run-86af61e6 review catch): when the matched run was accepted via the
        # tracker-only reconcile, the human's ack decision must carry that fact too - the reviewed
        # tree id was reused across a tracker-only reconcile, not reviewed against the exact
        # current tree. Withholding it from the degraded-ack paths hid a material fact from the
        # human decision while the fresh path announced it.
        return New-ContinuousCoReviewSignoffGateDecision -Decision 'allow' -Reason 'degraded-evidence-acknowledged' -Message ("{0}Signoff allowed on DEGRADED review evidence (completeness={1}, independence={2}, budget={3}) under a recorded human acknowledgement." -f [string]$honestyBypassNote, $labels.completeness, $labels.independence, $labels.budget) -CurrentTreeId $digest.tree_id -MatchedRunId $matchedRunId -AnchorRef $anchor -EvidenceLabels $labels -Acknowledgement $ack
    }

    return New-ContinuousCoReviewSignoffGateDecision -Decision 'block' -Reason 'degraded-evidence-needs-ack' -Message ("{0}The matching co-review evidence is DEGRADED (completeness={1}, independence={2}, budget={3}); signing off on it needs a recorded human acknowledgement: run ``specrew review --ack-degraded {4} --ack-reason `"<why this assurance level is acceptable>`"`` (or re-run a full independent review)." -f [string]$honestyBypassNote, $labels.completeness, $labels.independence, $labels.budget, $matchedRunId) -CurrentTreeId $digest.tree_id -MatchedRunId $matchedRunId -AnchorRef $anchor -EvidenceLabels $labels
}

function Assert-ContinuousCoReviewSignoffGate {
    param(
        [Parameter(Mandatory)]
        [string] $RepoRoot,

        [AllowEmptyString()][string] $TrunkName = '',

        [string[]] $ExcludedPathPatterns = @(),

        [AllowNull()] $OverrideAuthorization,

        [AllowNull()] $DegradedAcknowledgement
    )

    $decision = Get-ContinuousCoReviewSignoffGateDecision -RepoRoot $RepoRoot -TrunkName $TrunkName -ExcludedPathPatterns $ExcludedPathPatterns -OverrideAuthorization $OverrideAuthorization -DegradedAcknowledgement $DegradedAcknowledgement
    if ($decision.decision -eq 'block') {
        throw "[continuous-co-review-gate] review-signoff refused ($($decision.reason)): $($decision.message)"
    }

    return $decision
}