scripts/internal/continuous-co-review/verification-plan-materializer.ps1
|
# FR-049 / T063 — project source capture plus hash-guarded materialization for the pure T062 selector. if (-not (Get-Command -Name 'Select-ContinuousCoReviewVerificationPlan' -ErrorAction SilentlyContinue)) { . (Join-Path $PSScriptRoot 'verification-plan-supplier.ps1') } function Resolve-ContinuousCoReviewVerificationPlanCatalogPath { param([Parameter(Mandatory)][string]$RepoRoot, [AllowNull()][string]$CatalogPath) if (-not [string]::IsNullOrWhiteSpace($CatalogPath)) { return $CatalogPath } $deployed = Join-Path $RepoRoot '.specify/extensions/specrew-speckit/data/verification-plan-catalog.json' if (Test-Path -LiteralPath $deployed -PathType Leaf) { return $deployed } $moduleRoot = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot '../../..')) return (Join-Path $moduleRoot 'extensions/specrew-speckit/data/verification-plan-catalog.json') } function Get-ContinuousCoReviewProjectMetadataIds { param([Parameter(Mandatory)][string]$RepoRoot) $packagePath = Join-Path $RepoRoot 'package.json' if (-not (Test-Path -LiteralPath $packagePath -PathType Leaf)) { return @() } try { $package = Get-Content -LiteralPath $packagePath -Raw | ConvertFrom-Json $scripts = $package.PSObject.Properties['scripts'] if ($null -eq $scripts -or $null -eq $scripts.Value) { return @() } $test = $scripts.Value.PSObject.Properties['test'] if ($null -eq $test -or -not ($test.Value -is [string]) -or [string]::IsNullOrWhiteSpace([string]$test.Value)) { return @() } $declared = ([string]$test.Value).Trim() # npm's generated placeholder is not a trustworthy verification command. if ($declared -match '(?i)no\s+test\s+specified' -or $declared -match '(?i)^echo\b.*\bexit\s+1\s*$') { return @() } return @('package-json.scripts-test.v1') } catch { return @() } } function Get-ContinuousCoReviewActiveProjectProviders { param([Parameter(Mandatory)][string]$RepoRoot) $path = Join-Path $RepoRoot '.specrew/repository-governance.yml' if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { return @() } $block = '' foreach ($rawLine in @(Get-Content -LiteralPath $path)) { $line = ($rawLine -replace '\s+#.*$', '').TrimEnd() if ($line -match '^(?<name>[A-Za-z0-9_-]+):\s*$') { $block = $Matches.name; continue } $value = $null if ($line -match '^provider:\s*(?<value>[^\s]+)\s*$') { $value = $Matches.value } elseif ($block -eq 'provider' -and $line -match '^\s+name:\s*(?<value>[^\s]+)\s*$') { $value = $Matches.value } elseif ($block -eq 'repository_governance' -and $line -match '^\s{2}provider:\s*(?<value>[^\s]+)\s*$') { $value = $Matches.value } if (-not [string]::IsNullOrWhiteSpace($value)) { $normalized = $value.Trim('"', "'", ' ').ToLowerInvariant() if (Test-ContinuousCoReviewSupplierIdentity $normalized) { return @($normalized) } return @() } } return @() } function Write-ContinuousCoReviewVerificationPlanFile { param([Parameter(Mandatory)][string]$Path, [Parameter(Mandatory)][AllowEmptyString()][string]$Content) $parent = Split-Path -Parent $Path if (-not (Test-Path -LiteralPath $parent -PathType Container)) { New-Item -ItemType Directory -Path $parent -Force | Out-Null } $temp = Join-Path $parent ('.verification-plan-' + [guid]::NewGuid().ToString('N') + '.tmp') try { [System.IO.File]::WriteAllText($temp, $Content, [System.Text.UTF8Encoding]::new($false)) [System.IO.File]::Move($temp, $Path, $true) } finally { if (Test-Path -LiteralPath $temp) { Remove-Item -LiteralPath $temp -Force -ErrorAction SilentlyContinue } } } function New-ContinuousCoReviewVerificationMaterializationResult { param( [Parameter(Mandatory)][string]$State, [Parameter(Mandatory)][string]$Action, [Parameter(Mandatory)][bool]$Mutated, [AllowNull()][string]$Warning, [AllowNull()]$Selection, [Parameter(Mandatory)][string]$PlanPath, [Parameter(Mandatory)][string]$MarkerPath ) return [pscustomobject][ordered]@{ schema_version = '1.0' state = $State action = $Action mutated = $Mutated warning = if ([string]::IsNullOrWhiteSpace($Warning)) { $null } else { $Warning } plan_path = $PlanPath marker_path = $MarkerPath selection = $Selection } } function Invoke-ContinuousCoReviewVerificationPlanMaterialization { [CmdletBinding()] param( [Parameter(Mandatory)][string]$RepoRoot, [AllowNull()][string]$CatalogPath, [AllowNull()][string]$QualityProfileId, [AllowNull()][string[]]$ActiveProviders, [switch]$PreviewOnly ) $root = [System.IO.Path]::GetFullPath($RepoRoot) $planPath = Join-Path $root '.specrew/verification-plan.json' $markerPath = Join-Path $root '.specrew/verification-plan.generated.json' $resolvedCatalog = Resolve-ContinuousCoReviewVerificationPlanCatalogPath -RepoRoot $root -CatalogPath $CatalogPath if (-not (Test-Path -LiteralPath $resolvedCatalog -PathType Leaf)) { return New-ContinuousCoReviewVerificationMaterializationResult -State invalid -Action 'catalog-missing' -Mutated $false -Warning 'Verification-plan catalog is missing; no project plan was changed.' -Selection $null -PlanPath $planPath -MarkerPath $markerPath } try { $catalog = Get-Content -LiteralPath $resolvedCatalog -Raw | ConvertFrom-Json } catch { return New-ContinuousCoReviewVerificationMaterializationResult -State invalid -Action 'catalog-invalid' -Mutated $false -Warning 'Verification-plan catalog is malformed; no project plan was changed.' -Selection $null -PlanPath $planPath -MarkerPath $markerPath } $planExists = Test-Path -LiteralPath $planPath -PathType Leaf $markerExists = Test-Path -LiteralPath $markerPath -PathType Leaf $generatedAndUnmodified = $false if ($planExists -and $markerExists) { try { $marker = Get-Content -LiteralPath $markerPath -Raw | ConvertFrom-Json $expectedHash = [string](Get-ContinuousCoReviewSupplierProp $marker 'generated_content_hash') $currentHash = Get-ContinuousCoReviewSupplierSha256 (Get-Content -LiteralPath $planPath -Raw) $generatedAndUnmodified = ([string](Get-ContinuousCoReviewSupplierProp $marker 'schema_version') -ceq '1.0') -and ($expectedHash -cmatch '^[0-9a-f]{64}$') -and ($currentHash -ceq $expectedHash) } catch { $generatedAndUnmodified = $false } if (-not $generatedAndUnmodified) { return New-ContinuousCoReviewVerificationMaterializationResult -State 'preserved-modified' -Action 'preserved-modified-generated-plan' -Mutated $false -Warning 'The generated verification plan or its ownership marker changed; preserving both files byte-for-byte. Reconcile them explicitly before refresh.' -Selection $null -PlanPath $planPath -MarkerPath $markerPath } } elseif ($planExists) { $explicit = $null try { $explicit = Get-Content -LiteralPath $planPath -Raw | ConvertFrom-Json } catch { $explicit = $null } $selection = Select-ContinuousCoReviewVerificationPlan -RepoRoot $root -Catalog $catalog -ExplicitPlanPresent -ExplicitPlan $explicit $action = if ($selection.state -eq 'selected') { 'preserved-explicit-plan' } else { 'preserved-invalid-explicit-plan' } $warning = if ($selection.state -eq 'selected') { $null } else { $selection.action } return New-ContinuousCoReviewVerificationMaterializationResult -State $selection.state -Action $action -Mutated $false -Warning $warning -Selection $selection -PlanPath $planPath -MarkerPath $markerPath } elseif ($markerExists) { if (-not $PreviewOnly) { Remove-Item -LiteralPath $markerPath -Force } $markerExists = $false } $metadataIds = @(Get-ContinuousCoReviewProjectMetadataIds -RepoRoot $root) $providers = if ($PSBoundParameters.ContainsKey('ActiveProviders')) { @($ActiveProviders) } else { @(Get-ContinuousCoReviewActiveProjectProviders -RepoRoot $root) } $selection = Select-ContinuousCoReviewVerificationPlan -RepoRoot $root -Catalog $catalog -DetectedMetadataIds $metadataIds -QualityProfileId $QualityProfileId -ActiveProviders $providers if ($selection.state -ne 'selected') { $removed = $false if ($generatedAndUnmodified) { $removed = $true if (-not $PreviewOnly) { Remove-Item -LiteralPath $planPath -Force Remove-Item -LiteralPath $markerPath -Force -ErrorAction SilentlyContinue } } $action = if ($removed) { if ($PreviewOnly) { 'would-remove-unconfigured-generated-plan' } else { 'removed-unconfigured-generated-plan' } } else { 'verification-not-configured' } if ($selection.state -eq 'invalid') { $action = if ($removed) { 'removed-invalid-generated-plan' } else { 'verification-plan-invalid' } } return New-ContinuousCoReviewVerificationMaterializationResult -State $selection.state -Action $action -Mutated ($removed -and -not $PreviewOnly) -Warning $selection.action -Selection $selection -PlanPath $planPath -MarkerPath $markerPath } $planContent = ConvertTo-ContinuousCoReviewSupplierCanonicalJson $selection.plan $catalogId = [string](Get-ContinuousCoReviewSupplierProp $catalog 'catalog_id') $markerObject = [ordered]@{ schema_version = '1.0' selection_id = $selection.selection_id generated_content_hash = $selection.generated_content_hash plan_id = $selection.plan_id catalog_id = $catalogId source_kind = $selection.source_kind source_identity = $selection.source_identity } $markerContent = ConvertTo-ContinuousCoReviewSupplierCanonicalJson $markerObject if ($generatedAndUnmodified -and (Get-Content -LiteralPath $planPath -Raw) -ceq $planContent -and (Get-Content -LiteralPath $markerPath -Raw) -ceq $markerContent) { return New-ContinuousCoReviewVerificationMaterializationResult -State selected -Action 'generated-plan-current' -Mutated $false -Warning $null -Selection $selection -PlanPath $planPath -MarkerPath $markerPath } $action = if ($generatedAndUnmodified) { if ($PreviewOnly) { 'would-refresh-generated-plan' } else { 'refreshed-generated-plan' } } else { if ($PreviewOnly) { 'would-create-generated-plan' } else { 'created-generated-plan' } } if (-not $PreviewOnly) { Write-ContinuousCoReviewVerificationPlanFile -Path $planPath -Content $planContent Write-ContinuousCoReviewVerificationPlanFile -Path $markerPath -Content $markerContent } return New-ContinuousCoReviewVerificationMaterializationResult -State selected -Action $action -Mutated (-not $PreviewOnly) -Warning $null -Selection $selection -PlanPath $planPath -MarkerPath $markerPath } |