Files
ai-contract-platform/scripts/export-phase13-reviewer-kit.ps1
T
2026-05-02 10:36:49 +08:00

461 lines
19 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
param(
[string]$TaskPackagePath = "",
[string]$ArtifactRoot = "",
[switch]$FailOnBlocked
)
$ErrorActionPreference = "Stop"
$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
$timestamp = Get-Date -Format "yyyy-MM-ddTHH-mm-ss-fff"
function Resolve-LatestArtifactDir {
param([string]$RootRelativePath)
$root = Join-Path $repoRoot $RootRelativePath
$latest = Get-ChildItem -LiteralPath $root -Directory -ErrorAction SilentlyContinue |
Sort-Object Name -Descending |
Select-Object -First 1
if ($latest) {
return $latest.FullName
}
return ""
}
if ([string]::IsNullOrWhiteSpace($TaskPackagePath)) {
$TaskPackagePath = Resolve-LatestArtifactDir -RootRelativePath "project-docs\qa-artifacts\phase13-review-task-package-gate"
}
if (-not [string]::IsNullOrWhiteSpace($TaskPackagePath) -and -not [System.IO.Path]::IsPathRooted($TaskPackagePath)) {
$TaskPackagePath = Join-Path $repoRoot $TaskPackagePath
}
if ([string]::IsNullOrWhiteSpace($ArtifactRoot)) {
$ArtifactRoot = Join-Path $repoRoot "project-docs\qa-artifacts\phase13-reviewer-kit-export"
}
if (-not [System.IO.Path]::IsPathRooted($ArtifactRoot)) {
$ArtifactRoot = Join-Path $repoRoot $ArtifactRoot
}
$artifactDir = Join-Path $ArtifactRoot $timestamp
$kitRoot = Join-Path $artifactDir "reviewer-kits"
$legalKitDir = Join-Path $kitRoot "legal"
$businessKitDir = Join-Path $kitRoot "business"
$ackKitDir = Join-Path $kitRoot "ack"
$returnDir = Join-Path $kitRoot "return-templates"
$auditDir = Join-Path $kitRoot "audit"
New-Item -ItemType Directory -Force -Path $legalKitDir, $businessKitDir, $ackKitDir, $returnDir, $auditDir | Out-Null
$summaryJson = Join-Path $artifactDir "phase13-reviewer-kit-export-summary.json"
$summaryMd = Join-Path $artifactDir "phase13-reviewer-kit-export-summary.md"
$manifestCsv = Join-Path $artifactDir "reviewer-kit-manifest.csv"
$hashManifestCsv = Join-Path $artifactDir "reviewer-kit-sha256-manifest.csv"
$returnChecklistMd = Join-Path $artifactDir "reviewer-kit-return-checklist.md"
$gitStatusPath = Join-Path $artifactDir "git-status.txt"
$checks = New-Object System.Collections.Generic.List[object]
$manifestRows = New-Object System.Collections.Generic.List[object]
$hashRows = New-Object System.Collections.Generic.List[object]
function Add-Check {
param(
[string]$Key,
[string]$Label,
[ValidateSet("pass", "fail", "warn", "skip", "blocked")]
[string]$Status,
[string]$Detail,
[bool]$Required = $true,
[hashtable]$Meta = @{}
)
$checks.Add([ordered]@{
key = $Key
label = $Label
status = $Status
required = $Required
detail = $Detail
meta = $Meta
}) | Out-Null
}
function Save-Json {
param([string]$Path, [object]$Value)
$Value | ConvertTo-Json -Depth 50 | Set-Content -Encoding UTF8 -Path $Path
}
function Read-JsonFile {
param([string]$Path)
if ([string]::IsNullOrWhiteSpace($Path) -or -not (Test-Path -LiteralPath $Path -PathType Leaf)) {
return $null
}
return Get-Content -LiteralPath $Path -Raw -Encoding UTF8 | ConvertFrom-Json
}
function Import-CsvRows {
param([string]$Path)
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
return @()
}
return @(Import-Csv -LiteralPath $Path)
}
function Get-MetricValue {
param([object]$Summary, [string]$Name, [object]$Default = 0)
if ($null -eq $Summary -or $null -eq $Summary.metrics) {
return $Default
}
$property = $Summary.metrics.PSObject.Properties[$Name]
if ($null -eq $property -or $null -eq $property.Value) {
return $Default
}
return $property.Value
}
function Convert-OverallStatusText {
param([string]$Status)
switch ($Status) {
"PASS" { return "通过" }
"WARN" { return "通过但有警告" }
"FAIL" { return "未通过" }
"BLOCKED" { return "阻塞" }
default { return $Status }
}
}
function Convert-CheckStatusText {
param([string]$Status)
switch ($Status) {
"pass" { return "通过" }
"fail" { return "失败" }
"warn" { return "警告" }
"skip" { return "跳过" }
"blocked" { return "阻塞" }
default { return $Status }
}
}
function Add-Material {
param(
[string]$SourcePath,
[string]$TargetPath,
[string]$Kind,
[bool]$Required = $true
)
if (Test-Path -LiteralPath $SourcePath -PathType Leaf) {
Copy-Item -LiteralPath $SourcePath -Destination $TargetPath -Force
$manifestRows.Add([pscustomobject][ordered]@{
kind = $Kind
source = $SourcePath
target = $TargetPath
required = $Required
status = "included"
}) | Out-Null
return $true
}
$manifestRows.Add([pscustomobject][ordered]@{
kind = $Kind
source = $SourcePath
target = $TargetPath
required = $Required
status = "missing"
}) | Out-Null
return $false
}
function Write-ReturnTemplate {
param(
[object[]]$Rows,
[string]$Path,
[string]$Role
)
$Rows | ForEach-Object {
[pscustomobject][ordered]@{
review_id = $_.review_id
asset_type = $_.asset_type
asset_id = $_.asset_id
asset_name = $_.asset_name
reviewer_role = $Role
reviewer = $_.assignee
review_date = ""
review_result = "PENDING"
required_changes = ""
evidence_path = $_.evidence_form_path
allow_review_import = "false"
allow_production_release = "false"
notes = "由真实复核人填写;未填写前不得改为 APPROVED"
}
} | Export-Csv -LiteralPath $Path -NoTypeInformation -Encoding UTF8
}
function Write-AckReturnTemplate {
param([object[]]$Rows, [string]$Path)
$Rows | ForEach-Object {
[pscustomobject][ordered]@{
work_order_id = $_.work_order_id
recipient_role = $_.recipient_role
recipient = $_.recipient
handoff_owner = $_.handoff_owner
acknowledged = "false"
ack_date = ""
package_path = $_.package_path
missing_materials = ""
notes = "由接收人填写;未签收前不得视为真实复核启动"
}
} | Export-Csv -LiteralPath $Path -NoTypeInformation -Encoding UTF8
}
if (-not (Test-Path "$HOME\.Codex\skills\gstack\bin")) {
Add-Check -Key "gstack" -Label "gstack 前置检查" -Status "fail" -Detail "gstack 缺失"
} else {
Add-Check -Key "gstack" -Label "gstack 前置检查" -Status "pass" -Detail "GSTACK_OK"
}
$branch = (git -C $repoRoot rev-parse --abbrev-ref HEAD).Trim()
$commit = (git -C $repoRoot rev-parse HEAD).Trim()
$remoteUrl = (git -C $repoRoot remote get-url origin).Trim()
$gitStatus = git -C $repoRoot status --short
($gitStatus -join [Environment]::NewLine) | Set-Content -Encoding UTF8 -Path $gitStatusPath
if ($remoteUrl -match "192\.168\.3\.90") {
Add-Check -Key "origin_remote" -Label "origin 远端仓库" -Status "pass" -Detail "origin 指向 192.168.3.90" -Meta @{ branch = $branch; commit = $commit; remote = $remoteUrl }
} else {
Add-Check -Key "origin_remote" -Label "origin 远端仓库" -Status "fail" -Detail "origin 未指向 192.168.3.90" -Meta @{ branch = $branch; commit = $commit; remote = $remoteUrl }
}
if ($gitStatus) {
Add-Check -Key "working_tree" -Label "工作区状态" -Status "warn" -Required $false -Detail "工作区存在未提交改动;派发包门禁按证据记录" -Meta @{ artifact = $gitStatusPath }
} else {
Add-Check -Key "working_tree" -Label "工作区状态" -Status "pass" -Detail "工作区干净" -Meta @{ artifact = $gitStatusPath }
}
$taskSummaryPath = if ([string]::IsNullOrWhiteSpace($TaskPackagePath)) { "" } else { Join-Path $TaskPackagePath "phase13-review-task-package-gate-summary.json" }
$taskSummary = Read-JsonFile -Path $taskSummaryPath
if ($null -ne $taskSummary) {
Add-Check -Key "task_package_source" -Label "任务包来源" -Status "pass" -Detail "读取到任务包摘要,状态=$($taskSummary.status)" -Meta @{ path = $taskSummaryPath }
} else {
Add-Check -Key "task_package_source" -Label "任务包来源" -Status "fail" -Detail "缺少任务包摘要" -Meta @{ path = $taskSummaryPath }
}
$legalWorkOrdersPath = Join-Path $TaskPackagePath "legal-review-work-orders.csv"
$businessWorkOrdersPath = Join-Path $TaskPackagePath "business-review-work-orders.csv"
$ackWorkOrdersPath = Join-Path $TaskPackagePath "handoff-ack-work-orders.csv"
$acceptanceBoardPath = Join-Path $TaskPackagePath "review-acceptance-tracking-board.csv"
$dispatchManifestPath = Join-Path $TaskPackagePath "review-dispatch-manifest.csv"
$sourceWorkOrdersDir = Join-Path $TaskPackagePath "work-orders"
$legalRows = Import-CsvRows -Path $legalWorkOrdersPath
$businessRows = Import-CsvRows -Path $businessWorkOrdersPath
$ackRows = Import-CsvRows -Path $ackWorkOrdersPath
$materialsOk = $true
$materialsOk = (Add-Material -SourcePath $legalWorkOrdersPath -TargetPath (Join-Path $legalKitDir "legal-review-work-orders.csv") -Kind "legal_work_orders") -and $materialsOk
$materialsOk = (Add-Material -SourcePath $businessWorkOrdersPath -TargetPath (Join-Path $businessKitDir "business-review-work-orders.csv") -Kind "business_work_orders") -and $materialsOk
$materialsOk = (Add-Material -SourcePath $ackWorkOrdersPath -TargetPath (Join-Path $ackKitDir "handoff-ack-work-orders.csv") -Kind "ack_work_orders") -and $materialsOk
$materialsOk = (Add-Material -SourcePath $acceptanceBoardPath -TargetPath (Join-Path $auditDir "review-acceptance-tracking-board.csv") -Kind "acceptance_board") -and $materialsOk
$materialsOk = (Add-Material -SourcePath $dispatchManifestPath -TargetPath (Join-Path $auditDir "review-dispatch-manifest.csv") -Kind "dispatch_manifest") -and $materialsOk
foreach ($subdir in @("legal", "business", "ack")) {
$sourceDir = Join-Path $sourceWorkOrdersDir $subdir
$targetDir = Join-Path $kitRoot $subdir
if (Test-Path -LiteralPath $sourceDir -PathType Container) {
$sourceFiles = @(Get-ChildItem -LiteralPath $sourceDir -File -ErrorAction SilentlyContinue)
foreach ($sourceFile in $sourceFiles) {
Copy-Item -LiteralPath $sourceFile.FullName -Destination $targetDir -Force
}
$manifestRows.Add([pscustomobject][ordered]@{
kind = "$subdir`_markdown_work_orders"
source = $sourceDir
target = $targetDir
required = $true
status = if ($sourceFiles.Count -gt 0) { "included:$($sourceFiles.Count)" } else { "missing" }
}) | Out-Null
if ($sourceFiles.Count -eq 0) {
$materialsOk = $false
}
} else {
$materialsOk = $false
$manifestRows.Add([pscustomobject][ordered]@{
kind = "$subdir`_markdown_work_orders"
source = $sourceDir
target = $targetDir
required = $true
status = "missing"
}) | Out-Null
}
}
$legalReturnTemplate = Join-Path $returnDir "legal-review-result-return.csv"
$businessReturnTemplate = Join-Path $returnDir "business-review-result-return.csv"
$ackReturnTemplate = Join-Path $returnDir "handoff-ack-return.csv"
Write-ReturnTemplate -Rows $legalRows -Path $legalReturnTemplate -Role "法务"
Write-ReturnTemplate -Rows $businessRows -Path $businessReturnTemplate -Role "业务"
Write-AckReturnTemplate -Rows $ackRows -Path $ackReturnTemplate
$manifestRows.Add([pscustomobject][ordered]@{ kind = "legal_return_template"; source = ""; target = $legalReturnTemplate; required = $true; status = "generated" }) | Out-Null
$manifestRows.Add([pscustomobject][ordered]@{ kind = "business_return_template"; source = ""; target = $businessReturnTemplate; required = $true; status = "generated" }) | Out-Null
$manifestRows.Add([pscustomobject][ordered]@{ kind = "ack_return_template"; source = ""; target = $ackReturnTemplate; required = $true; status = "generated" }) | Out-Null
$checklist = New-Object System.Collections.Generic.List[string]
$checklist.Add("# 阶段 13 复核派发包回收清单") | Out-Null
$checklist.Add("") | Out-Null
$checklist.Add("## 派发前") | Out-Null
$checklist.Add("") | Out-Null
$checklist.Add("1. 确认真实复核输入接入门禁通过。") | Out-Null
$checklist.Add("2. 确认任务包门禁为 PASS 或 WARN,且可派发工作项不为 0。") | Out-Null
$checklist.Add("3. 将 legal、business、ack 三个目录分别交给对应角色。") | Out-Null
$checklist.Add("4. 保存本目录哈希清单,作为派发前证据。") | Out-Null
$checklist.Add("") | Out-Null
$checklist.Add("## 回收后") | Out-Null
$checklist.Add("") | Out-Null
$checklist.Add("1. 回收 `return-templates` 中的结果文件。") | Out-Null
$checklist.Add("2. 运行 `apply-phase13-review-results.ps1` 做 DryRun 校验。") | Out-Null
$checklist.Add("3. 运行进度、审计归档、总控和生产发布门禁。") | Out-Null
$checklist.Add("4. 未通过门禁前不得把任何资产改为生产可发布。") | Out-Null
$checklist | Set-Content -Encoding UTF8 -Path $returnChecklistMd
$manifestRows.Add([pscustomobject][ordered]@{ kind = "return_checklist"; source = ""; target = $returnChecklistMd; required = $true; status = "generated" }) | Out-Null
$manifestRows | Export-Csv -LiteralPath $manifestCsv -NoTypeInformation -Encoding UTF8
$kitFiles = @(Get-ChildItem -LiteralPath $kitRoot -Recurse -File -ErrorAction SilentlyContinue)
foreach ($file in $kitFiles) {
$hash = Get-FileHash -LiteralPath $file.FullName -Algorithm SHA256
$relative = $file.FullName.Substring($kitRoot.Length).TrimStart("\")
$hashRows.Add([pscustomobject][ordered]@{
relative_path = $relative
length = $file.Length
sha256 = $hash.Hash
}) | Out-Null
}
$hashRows | Export-Csv -LiteralPath $hashManifestCsv -NoTypeInformation -Encoding UTF8
if ($materialsOk) {
Add-Check -Key "kit_materials" -Label "派发包材料" -Status "pass" -Detail "必需派发材料齐套"
} else {
Add-Check -Key "kit_materials" -Label "派发包材料" -Status "fail" -Detail "必需派发材料缺失"
}
if ($kitFiles.Count -gt 0 -and $hashRows.Count -eq $kitFiles.Count) {
Add-Check -Key "hash_manifest" -Label "派发包哈希清单" -Status "pass" -Detail "已生成 $($hashRows.Count) 条 SHA256 留痕"
} else {
Add-Check -Key "hash_manifest" -Label "派发包哈希清单" -Status "fail" -Detail "哈希清单数量异常"
}
$taskReadyOrders = [int](Get-MetricValue -Summary $taskSummary -Name "ready_to_dispatch_orders")
$taskAcceptanceRows = [int](Get-MetricValue -Summary $taskSummary -Name "acceptance_tracking_rows")
if ($taskAcceptanceRows -gt 0 -and $taskReadyOrders -eq $taskAcceptanceRows -and @("PASS", "WARN") -contains [string]$taskSummary.status) {
Add-Check -Key "dispatch_readiness" -Label "真实派发条件" -Status "pass" -Detail "任务包已具备真实派发条件"
} else {
Add-Check -Key "dispatch_readiness" -Label "真实派发条件" -Status "blocked" -Detail "任务包尚未具备真实派发条件:ready=$taskReadyOrders / acceptance=$taskAcceptanceRows / status=$($taskSummary.status)"
}
$approvalLeaks = @($kitFiles | Select-String -Pattern "APPROVED_FOR_PRODUCTION|APPROVED_FOR_REVIEW_IMPORT" -SimpleMatch -List -ErrorAction SilentlyContinue)
if ($approvalLeaks.Count -eq 0) {
Add-Check -Key "no_approval_in_export" -Label "未在派发包中预置批准结论" -Status "pass" -Detail "派发包未预置批准结论"
} else {
Add-Check -Key "no_approval_in_export" -Label "未在派发包中预置批准结论" -Status "fail" -Detail "发现疑似预置批准结论:$($approvalLeaks.Count)"
}
$failedRequired = @($checks | Where-Object { $_.required -and $_.status -eq "fail" }).Count
$blockedRequired = @($checks | Where-Object { $_.required -and $_.status -eq "blocked" }).Count
$warnings = @($checks | Where-Object { $_.status -eq "warn" }).Count
$overallStatus = if ($failedRequired -gt 0) {
"FAIL"
} elseif ($blockedRequired -gt 0) {
"BLOCKED"
} elseif ($warnings -gt 0) {
"WARN"
} else {
"PASS"
}
$metrics = [ordered]@{
legal_work_orders = $legalRows.Count
business_work_orders = $businessRows.Count
ack_work_orders = $ackRows.Count
kit_files = $kitFiles.Count
hash_manifest_rows = $hashRows.Count
manifest_rows = $manifestRows.Count
task_ready_to_dispatch_orders = $taskReadyOrders
task_acceptance_rows = $taskAcceptanceRows
}
$summary = [ordered]@{
name = "phase13-reviewer-kit-export"
status = $overallStatus
generated_at = (Get-Date).ToUniversalTime().ToString("o")
branch = $branch
commit = $commit
remote = $remoteUrl
task_package_path = $TaskPackagePath
checks = $checks.ToArray()
metrics = $metrics
artifacts = [ordered]@{
summary_json = $summaryJson
summary_md = $summaryMd
kit_root = $kitRoot
manifest = $manifestCsv
hash_manifest = $hashManifestCsv
return_checklist = $returnChecklistMd
git_status = $gitStatusPath
}
totals = [ordered]@{
total_checks = $checks.Count
failed_required = $failedRequired
blocked_required = $blockedRequired
warnings = $warnings
}
}
Save-Json -Path $summaryJson -Value $summary
$md = New-Object System.Collections.Generic.List[string]
$md.Add("# 阶段 13 复核派发包导出门禁") | Out-Null
$md.Add("") | Out-Null
$md.Add("- 门禁状态:$(Convert-OverallStatusText -Status $overallStatus)($overallStatus)") | Out-Null
$md.Add("- 生成时间:$($summary.generated_at)") | Out-Null
$md.Add("- 分支:$branch") | Out-Null
$md.Add("- 提交:$commit") | Out-Null
$md.Add("- 任务包来源:$TaskPackagePath") | Out-Null
$md.Add("- 派发包目录:$kitRoot") | Out-Null
$md.Add("") | Out-Null
$md.Add("## 统计") | Out-Null
$md.Add("") | Out-Null
$md.Add("| 指标 | 数量 |") | Out-Null
$md.Add("|---|---:|") | Out-Null
$md.Add("| 法务工作单 | $($metrics.legal_work_orders) |") | Out-Null
$md.Add("| 业务工作单 | $($metrics.business_work_orders) |") | Out-Null
$md.Add("| 签收工作单 | $($metrics.ack_work_orders) |") | Out-Null
$md.Add("| 派发包文件 | $($metrics.kit_files) |") | Out-Null
$md.Add("| 哈希清单 | $($metrics.hash_manifest_rows) |") | Out-Null
$md.Add("| 可派发工作项 | $($metrics.task_ready_to_dispatch_orders) / $($metrics.task_acceptance_rows) |") | Out-Null
$md.Add("") | Out-Null
$md.Add("## 检查项") | Out-Null
$md.Add("") | Out-Null
$md.Add("| 检查项 | 必需 | 状态 | 说明 |") | Out-Null
$md.Add("|---|---:|---|---|") | Out-Null
foreach ($check in $checks) {
$requiredText = if ($check.required) { "是" } else { "否" }
$md.Add("| $($check.label) | $requiredText | $(Convert-CheckStatusText -Status $check.status) | $($check.detail) |") | Out-Null
}
$md.Add("") | Out-Null
$md.Add("## 当前结论") | Out-Null
$md.Add("") | Out-Null
if ($overallStatus -eq "BLOCKED") {
$md.Add("派发包已导出并留痕,但由于真实复核输入和任务包派发条件未齐套,当前不能作为真实已派发证据。") | Out-Null
} elseif ($overallStatus -eq "PASS") {
$md.Add("派发包已具备真实派发条件,可按角色交付并进入签收回收。") | Out-Null
} else {
$md.Add("派发包门禁未完全通过,请查看检查项和清单。") | Out-Null
}
$md | Set-Content -Encoding UTF8 -Path $summaryMd
Write-Host "PHASE13_REVIEWER_KIT_EXPORT_$overallStatus $artifactDir"
if ($overallStatus -eq "FAIL") {
throw "阶段 13 复核派发包导出门禁未通过。查看:$summaryMd"
}
if ($overallStatus -eq "BLOCKED" -and $FailOnBlocked) {
throw "阶段 13 复核派发包导出门禁被阻塞。查看:$summaryMd"
}