fix: tighten formal launch approval handoff

This commit is contained in:
2026-05-13 11:20:10 +08:00
parent 1e051ebea9
commit e6837d05ca
7 changed files with 167 additions and 12 deletions
@@ -34,6 +34,17 @@ function Import-CsvRows {
return @(Import-Csv -LiteralPath $Path)
}
function ConvertTo-RepoRelativePath {
param([Parameter(Mandatory = $true)][string]$Path)
$repoFullPath = [System.IO.Path]::GetFullPath($repoRoot).TrimEnd([char[]]@("\", "/"))
$targetFullPath = [System.IO.Path]::GetFullPath($Path)
if ($targetFullPath.StartsWith($repoFullPath, [System.StringComparison]::OrdinalIgnoreCase)) {
return ($targetFullPath.Substring($repoFullPath.Length).TrimStart([char[]]@("\", "/")) -replace "\\", "/")
}
return ($targetFullPath -replace "\\", "/")
}
function Get-ReviewGroup {
param([Parameter(Mandatory = $true)][object]$Row)
@@ -160,6 +171,9 @@ if ([string]::IsNullOrWhiteSpace($ReturnBundleCsv)) {
if ([string]::IsNullOrWhiteSpace($OutputRoot)) {
$OutputRoot = Join-Path $repoRoot "project-docs\formal-launch-ops\reviewers\collaboration-packages"
}
if (-not [System.IO.Path]::IsPathRooted($OutputRoot)) {
$OutputRoot = Join-Path $repoRoot $OutputRoot
}
$timestamp = Get-Date -Format "yyyy-MM-ddTHH-mm-ss-fff"
if ([string]::IsNullOrWhiteSpace($ArtifactRoot)) {
@@ -191,8 +205,8 @@ foreach ($row in $rows) {
New-Item -ItemType Directory -Force -Path $evidenceDir | Out-Null
$safeRecordId = ([string]$row.record_id) -replace '[^A-Za-z0-9_.-]', '_'
$relativeEvidencePath = "project-docs/formal-launch-ops/reviewers/collaboration-packages/$group/evidence/$safeRecordId.md"
$fullEvidencePath = Join-Path $repoRoot $relativeEvidencePath
$fullEvidencePath = Join-Path $evidenceDir "$safeRecordId.md"
$relativeEvidencePath = ConvertTo-RepoRelativePath -Path $fullEvidencePath
$row.evidence_path = $relativeEvidencePath
$placeholder = Get-ReviewerPlaceholder -Group $group
@@ -240,14 +254,15 @@ foreach ($group in $groups) {
package_group = $group
rows = $groupRows.Count
evidence_templates = $evidenceCount
return_bundle = "project-docs/formal-launch-ops/reviewers/collaboration-packages/$group/return-bundle.csv"
handoff = "project-docs/formal-launch-ops/reviewers/collaboration-packages/$group/handoff.md"
return_bundle = ConvertTo-RepoRelativePath -Path $csvPath
handoff = ConvertTo-RepoRelativePath -Path $handoffPath
}) | Out-Null
}
$finalApproverDir = Join-Path $OutputRoot "FINAL_APPROVER"
New-Item -ItemType Directory -Force -Path $finalApproverDir | Out-Null
$finalChecklist = Join-Path $finalApproverDir "final-launch-approval-checklist.md"
$finalHandoff = Join-Path $finalApproverDir "handoff.md"
$finalLines = New-Object System.Collections.Generic.List[string]
$finalLines.Add("# Final Launch Approval Checklist") | Out-Null
$finalLines.Add("") | Out-Null
@@ -264,6 +279,31 @@ $finalLines.Add("- final approval name: TODO_REAL_REVIEWER") | Out-Null
$finalLines.Add("- final approval date: TODO_YYYY-MM-DD") | Out-Null
Write-Utf8File -Path $finalChecklist -Text ($finalLines -join [Environment]::NewLine)
$finalHandoffLines = New-Object System.Collections.Generic.List[string]
$finalHandoffLines.Add("# Review Collaboration Package: FINAL_APPROVER") | Out-Null
$finalHandoffLines.Add("") | Out-Null
$finalHandoffLines.Add("This package is for the final launch approver. It is intentionally not imported by the review return bundle script because final approval depends on all real review and signoff registers first.") | Out-Null
$finalHandoffLines.Add("") | Out-Null
$finalHandoffLines.Add("## How To Complete") | Out-Null
$finalHandoffLines.Add("") | Out-Null
$finalHandoffLines.Add("1. Confirm the preparation gate, approval gate checks, legal package, business package, ops package, and production signoff registers.") | Out-Null
$finalHandoffLines.Add("2. Update final-launch-approval-checklist.md with the real approver name and approval date.") | Out-Null
$finalHandoffLines.Add("3. Change EVIDENCE_STATUS: TEMPLATE to EVIDENCE_STATUS: READY only after real final approval.") | Out-Null
$finalHandoffLines.Add("4. Re-run scripts/run-formal-launch-approval-gate.ps1. The gate will reject this checklist while it is still TEMPLATE or contains TODO placeholders.") | Out-Null
$finalHandoffLines.Add("") | Out-Null
$finalHandoffLines.Add("## Safety Rule") | Out-Null
$finalHandoffLines.Add("") | Out-Null
$finalHandoffLines.Add("Do not use scripts/import-formal-launch-review-return-bundle.ps1 for this folder. Use the approval gate itself to validate the final checklist.") | Out-Null
Write-Utf8File -Path $finalHandoff -Text ($finalHandoffLines -join [Environment]::NewLine)
$summaryRows.Add([pscustomobject]@{
package_group = "FINAL_APPROVER"
rows = 1
evidence_templates = 1
return_bundle = ""
handoff = ConvertTo-RepoRelativePath -Path $finalHandoff
}) | Out-Null
$summaryCsv = Join-Path $OutputRoot "package-summary.csv"
$summaryRows | Export-Csv -LiteralPath $summaryCsv -NoTypeInformation -Encoding UTF8
@@ -106,7 +106,7 @@ function Test-EvidenceContentFinal {
function Require-Columns {
param(
[Parameter(Mandatory = $true)][object[]]$Rows,
[Parameter(Mandatory = $true)][AllowEmptyCollection()][object[]]$Rows,
[Parameter(Mandatory = $true)][string[]]$Columns
)
if ($Rows.Count -eq 0) {
@@ -187,11 +187,12 @@ $materialSignoffPath = Join-Path $repoRoot "project-docs\formal-launch-ops\signo
$templateSignoffPath = Join-Path $repoRoot "project-docs\formal-launch-ops\signoff\production-template-signoff-register.csv"
$knowledgeSignoffPath = Join-Path $repoRoot "project-docs\formal-launch-ops\signoff\production-knowledge-signoff-register.csv"
$bundleRows = Import-CsvRows -Path $BundleCsv
$reviewTasks = Import-CsvRows -Path $reviewTaskPath
$materialSignoffs = Import-CsvRows -Path $materialSignoffPath
$templateSignoffs = Import-CsvRows -Path $templateSignoffPath
$knowledgeSignoffs = Import-CsvRows -Path $knowledgeSignoffPath
$bundleCsvExists = Test-Path -LiteralPath $BundleCsv
$bundleRows = @(Import-CsvRows -Path $BundleCsv)
$reviewTasks = @(Import-CsvRows -Path $reviewTaskPath)
$materialSignoffs = @(Import-CsvRows -Path $materialSignoffPath)
$templateSignoffs = @(Import-CsvRows -Path $templateSignoffPath)
$knowledgeSignoffs = @(Import-CsvRows -Path $knowledgeSignoffPath)
$requiredColumns = @(
"record_type",
@@ -215,6 +216,9 @@ $requiredColumns = @(
$errors = New-Object System.Collections.Generic.List[string]
$warnings = New-Object System.Collections.Generic.List[string]
if (-not $bundleCsvExists) {
$errors.Add("bundle csv not found: $BundleCsv") | Out-Null
}
foreach ($errorText in (Require-Columns -Rows $bundleRows -Columns $requiredColumns)) {
$errors.Add($errorText) | Out-Null
}
@@ -105,6 +105,41 @@ function Test-EvidenceContentFinal {
return $true
}
function Get-FinalApprovalChecklistStatus {
param([Parameter(Mandatory = $true)][string]$Path)
if (-not (Test-Path -LiteralPath $Path)) {
return [ordered]@{
status = "fail"
detail = "final approval checklist missing"
}
}
$content = Get-Content -LiteralPath $Path -Raw -ErrorAction Stop
if ($content -match "EVIDENCE_STATUS:\s*TEMPLATE") {
return [ordered]@{
status = "blocked"
detail = "final approval checklist is still marked TEMPLATE"
}
}
if ($content -match "TODO_REAL_REVIEWER" -or $content -match "TODO_YYYY-MM-DD") {
return [ordered]@{
status = "blocked"
detail = "final approval checklist still contains reviewer placeholders"
}
}
if ($content -notmatch "EVIDENCE_STATUS:\s*READY") {
return [ordered]@{
status = "blocked"
detail = "final approval checklist is not marked READY"
}
}
return [ordered]@{
status = "pass"
detail = "final approval checklist is READY"
}
}
function Get-NumericPropertyValue {
param(
[object]$Object,
@@ -267,6 +302,7 @@ $reviewTaskPath = Join-Path $repoRoot "project-docs\formal-launch-ops\reviewers\
$materialSignoffPath = Join-Path $repoRoot "project-docs\formal-launch-ops\signoff\production-material-signoff-register.csv"
$templateSignoffPath = Join-Path $repoRoot "project-docs\formal-launch-ops\signoff\production-template-signoff-register.csv"
$knowledgeSignoffPath = Join-Path $repoRoot "project-docs\formal-launch-ops\signoff\production-knowledge-signoff-register.csv"
$finalApprovalChecklistPath = Join-Path $repoRoot "project-docs\formal-launch-ops\reviewers\collaboration-packages\FINAL_APPROVER\final-launch-approval-checklist.md"
if (-not (Test-Path "$HOME\.Codex\skills\gstack\bin")) {
Add-Check -Key "gstack" -Label "gstack preflight" -Status "fail" -Detail "gstack missing"
@@ -499,6 +535,9 @@ if ($gateMetrics.approved_clauses_missing_evidence -eq 0) {
Add-Check -Key "clause_evidence" -Label "clause evidence" -Status "blocked" -Detail "approved_clauses_missing_evidence=$($gateMetrics.approved_clauses_missing_evidence)"
}
$finalApprovalChecklistStatus = Get-FinalApprovalChecklistStatus -Path $finalApprovalChecklistPath
Add-Check -Key "final_launch_approval_checklist" -Label "final launch approval checklist" -Status $finalApprovalChecklistStatus.status -Detail $finalApprovalChecklistStatus.detail -Meta @{ path = $finalApprovalChecklistPath }
$checks |
ForEach-Object {
[pscustomobject]@{
@@ -566,6 +605,7 @@ $summary = [ordered]@{
material_signoff_register = $materialSignoffPath
template_signoff_register = $templateSignoffPath
knowledge_signoff_register = $knowledgeSignoffPath
final_approval_checklist = $finalApprovalChecklistPath
database_snapshot = if ($null -ne $databaseSnapshot) { $databaseSnapshot.artifact } else { $null }
}
}