scripts/internal/continuous-co-review/review-authority-cutover.ps1
|
$ErrorActionPreference = 'Stop' Set-StrictMode -Version Latest # F-198 / T041: the ONE production authority cutover seam. Consumers ask this module whether # the legacy or campaign path may act; they never infer authority from a missing file, a parse # failure, or the presence of legacy artifacts. The closed enum makes dual authority # unrepresentable. The explicit disabled state supports the two-step operational cutover: # legacy -> disabled -> campaign. function New-ContinuousCoReviewDisabledAuthorityDecision { param( [Parameter(Mandatory)][string]$Reason, [AllowNull()][string]$ConfigPath ) return [pscustomobject]@{ schema_version = '1.0' mode = 'disabled' valid = $false legacy_promotion_enabled = $false campaign_authority_enabled = $false reason = $Reason config_path = $ConfigPath } } function Get-ContinuousCoReviewAuthorityDecision { [CmdletBinding()] param([string]$ConfigPath) if ([string]::IsNullOrWhiteSpace($ConfigPath)) { $ConfigPath = Join-Path $PSScriptRoot 'review-authority-mode.json' } if (-not (Test-Path -LiteralPath $ConfigPath -PathType Leaf)) { return (New-ContinuousCoReviewDisabledAuthorityDecision -Reason 'authority-config-missing' -ConfigPath $ConfigPath) } try { $item = Get-Item -LiteralPath $ConfigPath -ErrorAction Stop if ($item.Length -gt 4096) { return (New-ContinuousCoReviewDisabledAuthorityDecision -Reason 'authority-config-too-large' -ConfigPath $ConfigPath) } $raw = Get-Content -LiteralPath $ConfigPath -Raw -Encoding UTF8 -ErrorAction Stop if ([string]::IsNullOrWhiteSpace($raw)) { return (New-ContinuousCoReviewDisabledAuthorityDecision -Reason 'authority-config-empty' -ConfigPath $ConfigPath) } $config = $raw | ConvertFrom-Json -Depth 4 -ErrorAction Stop } catch { return (New-ContinuousCoReviewDisabledAuthorityDecision -Reason 'authority-config-invalid-json' -ConfigPath $ConfigPath) } $properties = @($config.PSObject.Properties.Name) if (@($properties | Where-Object { $_ -notin @('schema_version', 'mode') }).Count -gt 0 -or -not ($properties -contains 'schema_version') -or -not ($properties -contains 'mode')) { return (New-ContinuousCoReviewDisabledAuthorityDecision -Reason 'authority-config-invalid-shape' -ConfigPath $ConfigPath) } if ([string]$config.schema_version -cne '1.0') { return (New-ContinuousCoReviewDisabledAuthorityDecision -Reason 'authority-config-unsupported-version' -ConfigPath $ConfigPath) } $mode = [string]$config.mode if ($mode -cnotin @('legacy', 'disabled', 'campaign')) { return (New-ContinuousCoReviewDisabledAuthorityDecision -Reason 'authority-config-invalid-mode' -ConfigPath $ConfigPath) } return [pscustomobject]@{ schema_version = '1.0' mode = $mode valid = $true legacy_promotion_enabled = ($mode -ceq 'legacy') campaign_authority_enabled = ($mode -ceq 'campaign') reason = ('authority-mode-' + $mode) config_path = $ConfigPath } } function Test-ContinuousCoReviewAuthorityEnabled { [CmdletBinding()] param( [Parameter(Mandatory)][ValidateSet('legacy', 'campaign')][string]$Authority, [AllowNull()]$Decision ) if ($null -eq $Decision) { $Decision = Get-ContinuousCoReviewAuthorityDecision } if (-not $Decision.valid) { return $false } if ($Authority -ceq 'legacy') { return [bool]$Decision.legacy_promotion_enabled } return [bool]$Decision.campaign_authority_enabled } function Resolve-ContinuousCoReviewAuthorityTransition { [CmdletBinding()] param( [Parameter(Mandatory)][ValidateSet('legacy', 'disabled', 'campaign')][string]$CurrentMode, [Parameter(Mandatory)][ValidateSet('legacy', 'disabled', 'campaign')][string]$ProposedMode, [ValidateRange(0, [int]::MaxValue)][int]$CampaignFactCount = 0 ) if ($CurrentMode -ceq $ProposedMode) { return [pscustomobject]@{ permitted = $true; reason = 'authority-mode-unchanged'; current_mode = $CurrentMode; proposed_mode = $ProposedMode } } $permitted = switch ("$CurrentMode->$ProposedMode") { 'legacy->disabled' { $true } 'disabled->campaign' { $true } 'campaign->disabled' { $true } 'disabled->legacy' { $CampaignFactCount -eq 0 } default { $false } } $reason = if (-not $permitted -and $CurrentMode -ceq 'legacy' -and $ProposedMode -ceq 'campaign') { 'campaign-cutover-requires-disabled-stage' } elseif (-not $permitted -and $ProposedMode -ceq 'legacy' -and $CampaignFactCount -gt 0) { 'legacy-reactivation-refused-after-campaign-facts' } elseif (-not $permitted) { 'authority-transition-refused' } else { 'authority-transition-permitted' } return [pscustomobject]@{ permitted = $permitted; reason = $reason; current_mode = $CurrentMode; proposed_mode = $ProposedMode } } function Set-ContinuousCoReviewAuthorityMode { [CmdletBinding()] param( [Parameter(Mandatory)][string]$ConfigPath, [Parameter(Mandatory)][ValidateSet('legacy', 'disabled', 'campaign')][string]$Mode, [Parameter(Mandatory)][string]$RepoRoot ) $current = Get-ContinuousCoReviewAuthorityDecision -ConfigPath $ConfigPath if (-not $current.valid) { throw "authority-cutover-current-state-invalid:$($current.reason)" } $root = (Resolve-Path -LiteralPath $RepoRoot).Path $campaignStore = Join-Path $root '.specrew/review/authority' $campaignFactCount = if ([IO.Directory]::Exists($campaignStore)) { @([IO.Directory]::EnumerateFiles($campaignStore, '*.json', [IO.SearchOption]::AllDirectories)).Count } else { 0 } $transition = Resolve-ContinuousCoReviewAuthorityTransition -CurrentMode ([string]$current.mode) -ProposedMode $Mode -CampaignFactCount $CampaignFactCount if (-not $transition.permitted) { throw "authority-cutover-refused:$($transition.reason)" } $json = ([ordered]@{ schema_version = '1.0'; mode = $Mode } | ConvertTo-Json -Compress) $full = [IO.Path]::GetFullPath($ConfigPath) $temp = $full + '.tmp-' + [guid]::NewGuid().ToString('N') try { [IO.File]::WriteAllText($temp, $json, [Text.UTF8Encoding]::new($false)) [IO.File]::Move($temp, $full, $true) } finally { if ([IO.File]::Exists($temp)) { [IO.File]::Delete($temp) } } return Get-ContinuousCoReviewAuthorityDecision -ConfigPath $full } |