scripts/internal/continuous-co-review/signoff-gate-wiring.ps1
|
$ErrorActionPreference = 'Stop' Set-StrictMode -Version Latest # T073 / FR-025 / SC-019 / SC-020: the boundary-sync wiring for the deterministic # co-review signoff gate floor. # # The DECISION logic lives in review-signoff-evidence-gate.ps1 (loaded via _load.ps1). This # file is the thin, testable SEAM that Invoke-SpecrewBoundaryStateSync calls so the 1500-line # sync function stays free of gate logic: # # 1. Get-ContinuousCoReviewGateEnforcementEnabled - returns TRUE by default. The old opt-in # scalar made the review-signoff backstop inert in exactly the host-switch/compaction case # it is supposed to catch. A present co_review_gate_enforcement key is now informational: # false/off/disabled no longer bypass review-signoff. # # 2. Invoke-ContinuousCoReviewSignoffGateIfEnabled - the boundary-sync entry point. It is a # no-op for every boundary except review-signoff. At review-signoff it dot-sources the # gate module, writes durable gate-decision evidence, and throws on any `block` decision. # Only a MISSING-gate-infrastructure failure (the _load.ps1 dot-source itself failing) is # re-wrapped as a clear error; a real block-refusal is NEVER disguised as infrastructure. # # This function emits NOTHING on the allow path. Invoke-SpecrewBoundaryStateSync returns a # single result object that a downstream dispatcher serializes; a stray emission here would make # that result a 2-element array and silently suppress the pending-verdict stop. function Get-ContinuousCoReviewGateEnforcementEnabled { <# .SYNOPSIS Return $true for the review-signoff hard gate. The old opt-in/off switch is retained only as a parsed compatibility surface and does not bypass review-signoff. #> param([Parameter(Mandatory = $true)][string]$ProjectRoot) $configPath = Join-Path $ProjectRoot '.specrew/config.yml' if (-not (Test-Path -LiteralPath $configPath -PathType Leaf)) { return $true } # Value grammar matches the sibling specrew_version reader in sync-boundary-state.ps1: strip an # optional single OR double quote, stop the value at a '#', and tolerate a trailing inline comment. # (The earlier `"?...[^"#]...` form silently dropped `true # comment` and `'true'` -> a governance # gate the operator believed was ON would stay OFF. 145 F1.) foreach ($line in Get-Content -LiteralPath $configPath -Encoding UTF8) { if ($line -match '^\s*co_review_gate_enforcement:\s*[''"]?(?<value>[^''"#]+?)[''"]?\s*(?:#.*)?$') { return $true } } return $true } function Write-ContinuousCoReviewSignoffGateDecisionEvidence { param( [Parameter(Mandatory = $true)][string]$ProjectRoot, [Parameter(Mandatory = $true)][string]$BoundaryType, [Parameter(Mandatory = $true)]$Decision ) $root = Join-Path $ProjectRoot '.specrew/review/signoff-gate' $historyRoot = Join-Path $root 'history' New-Item -ItemType Directory -Path $historyRoot -Force | Out-Null $recordedAt = [datetime]::UtcNow.ToString('yyyy-MM-ddTHH:mm:ssZ', [System.Globalization.CultureInfo]::InvariantCulture) $record = [pscustomobject][ordered]@{ schema_version = '1.0' boundary_type = $BoundaryType recorded_at = $recordedAt decision = $Decision } $json = $record | ConvertTo-Json -Depth 100 $latestPath = Join-Path $root 'latest.json' $historyPath = Join-Path $historyRoot ('{0}-{1}.json' -f ($recordedAt -replace '[:-]', ''), ([guid]::NewGuid().ToString('N').Substring(0, 8))) foreach ($path in @($latestPath, $historyPath)) { if (Get-Command -Name 'Write-SpecrewFileAtomic' -ErrorAction SilentlyContinue) { Write-SpecrewFileAtomic -Path $path -Content $json } else { [System.IO.File]::WriteAllText($path, $json, [System.Text.UTF8Encoding]::new($false)) } } } function Get-ContinuousCoReviewTrunkName { <# .SYNOPSIS Return the trunk branch name from .specrew/config.yml (`co_review_trunk`), default 'main'. 145 carry (T080): the gate's merge-base anchor resolves `<trunk>`/`origin/<trunk>`, so a non-`main` trunk (master/develop) otherwise fails CLOSED (anchor-unresolvable -> block). Making it configurable lets those repos opt into enforcement. Same value grammar as the enforcement reader (quote-strip + inline-comment tolerant). #> param([Parameter(Mandatory = $true)][string]$ProjectRoot) $configPath = Join-Path $ProjectRoot '.specrew/config.yml' if (-not (Test-Path -LiteralPath $configPath -PathType Leaf)) { return 'main' } foreach ($line in Get-Content -LiteralPath $configPath -Encoding UTF8) { if ($line -match '^\s*co_review_trunk:\s*[''"]?(?<value>[^''"#]+?)[''"]?\s*(?:#.*)?$') { $value = $Matches['value'].Trim() if (-not [string]::IsNullOrWhiteSpace($value)) { return $value } } } return 'main' } function Invoke-ContinuousCoReviewSignoffGateIfEnabled { <# .SYNOPSIS At the review-signoff boundary, refuse signoff unless the current reviewed-state matches fresh, anchor-covered co-review evidence (FR-025). A no-op for any other boundary. .DESCRIPTION Lets the gate's block-refusal throw propagate (fail-closed). Wraps ONLY the gate-module dot-source failure in a clear infrastructure error so a missing gate is never silently swallowed AND a real refusal is never mislabeled as infrastructure. #> param( [Parameter(Mandatory = $true)][string]$ProjectRoot, [Parameter(Mandatory = $true)][string]$BoundaryType ) if ($BoundaryType -ne 'review-signoff') { return } if (-not (Get-ContinuousCoReviewGateEnforcementEnabled -ProjectRoot $ProjectRoot)) { return } # Lazy-load the decision module (and its deps) only when the gate will actually fire. # A dot-source failure here is genuine missing infrastructure, not a co-review verdict. try { $gateLoaderPath = Join-Path $PSScriptRoot '_load.ps1' if (-not (Test-Path -LiteralPath $gateLoaderPath -PathType Leaf)) { throw "continuous-co-review gate loader not found at '$gateLoaderPath'." } . $gateLoaderPath } catch { throw ("[continuous-co-review-gate] review-signoff cannot be evaluated because the gate infrastructure failed to load: {0}" -f $_.Exception.Message) } # The throw on a `block` decision propagates verbatim (fail-closed). TrunkName is read from # config so non-`main`-trunk repos do not fail closed (145 carry T080). The decision is # persisted before either throw/allow so a failed signoff leaves inspectable evidence. $trunk = Get-ContinuousCoReviewTrunkName -ProjectRoot $ProjectRoot $decision = Get-ContinuousCoReviewSignoffGateDecision -RepoRoot $ProjectRoot -TrunkName $trunk Write-ContinuousCoReviewSignoffGateDecisionEvidence -ProjectRoot $ProjectRoot -BoundaryType $BoundaryType -Decision $decision if ($decision.decision -eq 'block') { throw "[continuous-co-review-gate] review-signoff refused ($($decision.reason)): $($decision.message)" } } |