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
@@ -607,3 +607,58 @@
- 审查规则签收:`approved_review_rules=0; min=8`
- 条款签收:`approved_clauses=0; min=8`
- 下一步应进入真实上线批准材料与签收台账补齐,而不是继续修改自动化脚本。
## 2026-05-13 正式上线批准协作包校验闭环补强
### 本轮完成项
- 已补强法务、业务、运营、最终批准四类协作包的执行边界:
- `FINAL_APPROVER/` 明确为最终批准清单包,不再被误当作 CSV 回填包。
- 新增 `FINAL_APPROVER/handoff.md`,说明最终批准人必须先核对准备门禁、批准门禁、三类复核包和生产签收台账。
- `package-summary.csv` 纳入 `FINAL_APPROVER`,协作包总览不再遗漏最终批准人。
- 已补强脚本闭环:
- `scripts/import-formal-launch-review-return-bundle.ps1` 对不存在或空回填包输出可读 `FAIL`,不再抛 PowerShell 参数绑定异常。
- `scripts/generate-formal-launch-review-collaboration-packages.ps1` 在自定义 `OutputRoot` 时把证据文件生成到指定目录,避免临时烟测误写正式协作包路径。
- `scripts/run-formal-launch-approval-gate.ps1` 新增 `final_launch_approval_checklist` 检查,最终批准清单仍为 `TEMPLATE` 或包含占位符时保持 `BLOCKED`。
### 本轮验证
- gstack 预检:`GSTACK_OK`。
- PowerShell Parser 检查:
- `scripts/import-formal-launch-review-return-bundle.ps1` 通过。
- `scripts/run-formal-launch-approval-gate.ps1` 通过。
- `scripts/generate-formal-launch-review-collaboration-packages.ps1` 通过。
- 三个真实角色回填包校验:
- `LEGAL_CORE`: `FORMAL_LAUNCH_REVIEW_RETURN_IMPORT_BLOCKED`
- `BUSINESS_TEMPLATE`: `FORMAL_LAUNCH_REVIEW_RETURN_IMPORT_BLOCKED`
- `OPS_MATERIAL`: `FORMAL_LAUNCH_REVIEW_RETURN_IMPORT_BLOCKED`
- 最终批准包误用 CSV 导入脚本时输出可读失败:
- `FORMAL_LAUNCH_REVIEW_RETURN_IMPORT_FAIL`
- 产物:`project-docs/qa-artifacts/formal-launch-review-return-import/2026-05-13T11-18-20-424/formal-launch-review-return-import-summary.md`
- 原因:`FINAL_APPROVER/return-bundle.csv` 不存在,且该目录应走最终批准清单和批准门禁验证。
- 协作包生成烟测:
- `FORMAL_LAUNCH_REVIEW_COLLABORATION_PACKAGES_PASS`
- 产物:`project-docs/qa-artifacts/formal-launch-review-collaboration-packages/2026-05-13T11-18-20-550/formal-launch-review-collaboration-packages-summary.md`
- 批准门禁聚焦复跑:
- `FORMAL_LAUNCH_APPROVAL_GATE_BLOCKED`
- 产物:`project-docs/qa-artifacts/formal-launch-approval-gate/2026-05-13T11-18-20-485/formal-launch-approval-summary.md`
### 当前状态更新
- 正式上线批准门禁仍然正确阻断,且阻断项更完整。
- 当前关键指标:
- `review_tasks=99`
- `missing_reviewer_tasks=99`
- `approved_review_tasks=0`
- `approved_materials=0`
- `approved_public_materials=0`
- `approved_templates=0`
- `approved_knowledge_cards=0`
- `approved_review_rules=0`
- `approved_clauses=0`
- `blocked_required=9`
### 当前阻塞项
- 仍需真实人员完成:
- 分配真实法务、业务、运营 reviewer。
- 完成真实审查执行和证据留存。
- 完成生产材料、公开法律文件、模板、知识卡、规则、条款签收。
- 最终上线批准人填写并签收 `FINAL_APPROVER/final-launch-approval-checklist.md`。
- 本轮没有伪造任何 reviewer、签收结论或批准证据。
@@ -0,0 +1,14 @@
# Review Collaboration Package: FINAL_APPROVER
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.
## How To Complete
1. Confirm the preparation gate, approval gate checks, legal package, business package, ops package, and production signoff registers.
2. Update final-launch-approval-checklist.md with the real approver name and approval date.
3. Change EVIDENCE_STATUS: TEMPLATE to EVIDENCE_STATUS: READY only after real final approval.
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.
## Safety Rule
Do not use scripts/import-formal-launch-review-return-bundle.ps1 for this folder. Use the approval gate itself to validate the final checklist.
@@ -17,9 +17,10 @@
2. 填写真实姓名、复核日期、复核状态、批准结论和修改意见。
3. 打开对应 `evidence/` 目录下的证据文件。
4. 完成复核后,将 `EVIDENCE_STATUS: TEMPLATE` 改为 `EVIDENCE_STATUS: READY`,并删除所有 `TODO_REAL_REVIEWER`。
5. 使用 `scripts/import-formal-launch-review-return-bundle.ps1` 校验回填包。
5. 法务、业务、运营包使用 `scripts/import-formal-launch-review-return-bundle.ps1` 校验回填包。
6. 校验通过后,才可以加 `-Apply` 写回主台账。
7. `FINAL_APPROVER/` 不使用 CSV 导入脚本;最终批准人填写 `final-launch-approval-checklist.md` 后,通过 `scripts/run-formal-launch-approval-gate.ps1` 统一验证。
## 安全规则
证据模板不能作为真实证据使用。只要证据文件仍包含 `EVIDENCE_STATUS: TEMPLATE` 或 `TODO_REAL_REVIEWER`,导入脚本和上线批准门禁都会拒绝放行。
证据模板不能作为真实证据使用。只要证据文件或最终批准清单仍包含 `EVIDENCE_STATUS: TEMPLATE`、`TODO_REAL_REVIEWER` 或 `TODO_YYYY-MM-DD`,导入脚本和上线批准门禁都会拒绝放行。
@@ -2,3 +2,4 @@
"LEGAL_CORE","101","101","project-docs/formal-launch-ops/reviewers/collaboration-packages/LEGAL_CORE/return-bundle.csv","project-docs/formal-launch-ops/reviewers/collaboration-packages/LEGAL_CORE/handoff.md"
"BUSINESS_TEMPLATE","38","38","project-docs/formal-launch-ops/reviewers/collaboration-packages/BUSINESS_TEMPLATE/return-bundle.csv","project-docs/formal-launch-ops/reviewers/collaboration-packages/BUSINESS_TEMPLATE/handoff.md"
"OPS_MATERIAL","3","3","project-docs/formal-launch-ops/reviewers/collaboration-packages/OPS_MATERIAL/return-bundle.csv","project-docs/formal-launch-ops/reviewers/collaboration-packages/OPS_MATERIAL/handoff.md"
"FINAL_APPROVER","1","1","","project-docs/formal-launch-ops/reviewers/collaboration-packages/FINAL_APPROVER/handoff.md"
1 package_group rows evidence_templates return_bundle handoff
2 LEGAL_CORE 101 101 project-docs/formal-launch-ops/reviewers/collaboration-packages/LEGAL_CORE/return-bundle.csv project-docs/formal-launch-ops/reviewers/collaboration-packages/LEGAL_CORE/handoff.md
3 BUSINESS_TEMPLATE 38 38 project-docs/formal-launch-ops/reviewers/collaboration-packages/BUSINESS_TEMPLATE/return-bundle.csv project-docs/formal-launch-ops/reviewers/collaboration-packages/BUSINESS_TEMPLATE/handoff.md
4 OPS_MATERIAL 3 3 project-docs/formal-launch-ops/reviewers/collaboration-packages/OPS_MATERIAL/return-bundle.csv project-docs/formal-launch-ops/reviewers/collaboration-packages/OPS_MATERIAL/handoff.md
5 FINAL_APPROVER 1 1 project-docs/formal-launch-ops/reviewers/collaboration-packages/FINAL_APPROVER/handoff.md
@@ -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 }
}
}