Files
ai-contract-platform/scripts/run-phase13-review-return-preflight.ps1
T
2026-05-02 10:42:09 +08:00

473 lines
20 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]$ReturnRoot = "",
[string]$ReviewRegisterPath = "",
[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($ReturnRoot)) {
$latestKit = Resolve-LatestArtifactDir -RootRelativePath "project-docs\qa-artifacts\phase13-reviewer-kit-export"
if (-not [string]::IsNullOrWhiteSpace($latestKit)) {
$ReturnRoot = Join-Path $latestKit "reviewer-kits\return-templates"
}
}
if (-not [string]::IsNullOrWhiteSpace($ReturnRoot) -and -not [System.IO.Path]::IsPathRooted($ReturnRoot)) {
$ReturnRoot = Join-Path $repoRoot $ReturnRoot
}
if ([string]::IsNullOrWhiteSpace($ReviewRegisterPath)) {
$ReviewRegisterPath = Join-Path $repoRoot "project-docs\phase13-legal-knowledge-template-library\inventories\expert-review-register.csv"
}
if (-not [System.IO.Path]::IsPathRooted($ReviewRegisterPath)) {
$ReviewRegisterPath = Join-Path $repoRoot $ReviewRegisterPath
}
if ([string]::IsNullOrWhiteSpace($ArtifactRoot)) {
$ArtifactRoot = Join-Path $repoRoot "project-docs\qa-artifacts\phase13-review-return-preflight"
}
if (-not [System.IO.Path]::IsPathRooted($ArtifactRoot)) {
$ArtifactRoot = Join-Path $repoRoot $ArtifactRoot
}
$artifactDir = Join-Path $ArtifactRoot $timestamp
New-Item -ItemType Directory -Force -Path $artifactDir | Out-Null
$summaryJson = Join-Path $artifactDir "phase13-review-return-preflight-summary.json"
$summaryMd = Join-Path $artifactDir "phase13-review-return-preflight-summary.md"
$sourceManifestCsv = Join-Path $artifactDir "return-source-manifest.csv"
$validationBoardCsv = Join-Path $artifactDir "return-validation-board.csv"
$blockersCsv = Join-Path $artifactDir "return-preflight-blockers.csv"
$diffPreviewCsv = Join-Path $artifactDir "return-diff-preview.csv"
$mergePlanCsv = Join-Path $artifactDir "return-merge-plan.csv"
$gitStatusPath = Join-Path $artifactDir "git-status.txt"
$checks = New-Object System.Collections.Generic.List[object]
$sourceRows = New-Object System.Collections.Generic.List[object]
$validationRows = New-Object System.Collections.Generic.List[object]
$blockerRows = New-Object System.Collections.Generic.List[object]
$diffRows = New-Object System.Collections.Generic.List[object]
$mergeRows = New-Object System.Collections.Generic.List[object]
$reviewRequiredColumns = @("review_id", "asset_type", "asset_id", "asset_name", "reviewer_role", "reviewer", "review_date", "review_result", "required_changes", "evidence_path", "allow_review_import", "allow_production_release", "notes")
$ackRequiredColumns = @("work_order_id", "recipient_role", "recipient", "handoff_owner", "acknowledged", "ack_date", "package_path", "missing_materials", "notes")
$allowedReviewResults = @("PENDING", "CHANGES_REQUIRED", "APPROVED_FOR_REVIEW_IMPORT", "APPROVED_FOR_PRODUCTION", "REJECTED")
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 Add-Blocker {
param([string]$Kind, [string]$Id, [string]$Severity, [string]$Blocker, [string]$Evidence)
$blockerRows.Add([pscustomobject][ordered]@{
kind = $Kind
id = $Id
severity = $Severity
blocker = $Blocker
evidence = $Evidence
}) | Out-Null
}
function Save-Json {
param([string]$Path, [object]$Value)
$Value | ConvertTo-Json -Depth 50 | Set-Content -Encoding UTF8 -Path $Path
}
function Import-CsvRows {
param([string]$Path)
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
return @()
}
return @(Import-Csv -LiteralPath $Path)
}
function Get-CsvHeader {
param([string]$Path)
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
return @()
}
$firstLine = Get-Content -LiteralPath $Path -Encoding UTF8 -TotalCount 1
if ([string]::IsNullOrWhiteSpace($firstLine)) {
return @()
}
return @($firstLine.Split(",") | ForEach-Object { $_.Trim().Trim('"') })
}
function Test-RequiredColumns {
param([string]$Path, [string[]]$Columns)
$header = Get-CsvHeader -Path $Path
return @($Columns | Where-Object { $header -notcontains $_ })
}
function Test-Truthy {
param([string]$Value)
if ([string]::IsNullOrWhiteSpace($Value)) {
return $false
}
return @("true", "yes", "1", "是", "已签收") -contains $Value.Trim().ToLowerInvariant()
}
function Test-PendingName {
param([string]$Value)
if ([string]::IsNullOrWhiteSpace($Value)) {
return $true
}
return ($Value -match "待指定|待确认|待复核|占位|示例")
}
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 }
}
}
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 }
}
if (Test-Path -LiteralPath $ReturnRoot -PathType Container) {
Add-Check -Key "return_root" -Label "回收文件目录" -Status "pass" -Detail "回收文件目录存在" -Meta @{ path = $ReturnRoot }
} else {
Add-Check -Key "return_root" -Label "回收文件目录" -Status "fail" -Detail "回收文件目录不存在" -Meta @{ path = $ReturnRoot }
}
$registerRows = Import-CsvRows -Path $ReviewRegisterPath
$registerByReviewId = @{}
foreach ($registerRow in $registerRows) {
if ($registerRow.review_id) {
$registerByReviewId[$registerRow.review_id] = $registerRow
}
}
$returnFiles = @(
[pscustomobject]@{ kind = "legal"; path = Join-Path $ReturnRoot "legal-review-result-return.csv"; required_columns = $reviewRequiredColumns },
[pscustomobject]@{ kind = "business"; path = Join-Path $ReturnRoot "business-review-result-return.csv"; required_columns = $reviewRequiredColumns },
[pscustomobject]@{ kind = "ack"; path = Join-Path $ReturnRoot "handoff-ack-return.csv"; required_columns = $ackRequiredColumns }
)
$allReviewReturns = New-Object System.Collections.Generic.List[object]
$ackReturns = @()
foreach ($returnFile in $returnFiles) {
$exists = Test-Path -LiteralPath $returnFile.path -PathType Leaf
$missing = if ($exists) { Test-RequiredColumns -Path $returnFile.path -Columns $returnFile.required_columns } else { $returnFile.required_columns }
$rows = if ($exists) { Import-CsvRows -Path $returnFile.path } else { @() }
$sourceRows.Add([pscustomobject][ordered]@{
kind = $returnFile.kind
path = $returnFile.path
exists = $exists
rows = $rows.Count
missing_columns = ($missing -join ";")
status = if ($exists -and $missing.Count -eq 0) { "ready" } else { "invalid" }
}) | Out-Null
if (-not $exists) {
Add-Blocker -Kind $returnFile.kind -Id $returnFile.path -Severity "P0" -Blocker "回收文件缺失" -Evidence $returnFile.path
} elseif ($missing.Count -gt 0) {
Add-Blocker -Kind $returnFile.kind -Id $returnFile.path -Severity "P0" -Blocker "回收文件字段缺失" -Evidence ($missing -join ",")
}
if ($returnFile.kind -eq "ack") {
$ackReturns = $rows
} else {
foreach ($row in $rows) {
$allReviewReturns.Add($row) | Out-Null
}
}
}
$sourceRows | Export-Csv -LiteralPath $sourceManifestCsv -NoTypeInformation -Encoding UTF8
foreach ($row in $allReviewReturns) {
$reviewResult = if ($row.review_result) { $row.review_result.Trim() } else { "" }
$reviewId = [string]$row.review_id
$inRegister = $registerByReviewId.ContainsKey($reviewId)
$resultAllowed = $allowedReviewResults -contains $reviewResult
$reviewerReady = -not (Test-PendingName -Value $row.reviewer)
$dateReady = -not [string]::IsNullOrWhiteSpace($row.review_date)
$evidenceReady = -not [string]::IsNullOrWhiteSpace($row.evidence_path)
$completed = ($reviewResult -ne "PENDING" -and $resultAllowed -and $reviewerReady -and $dateReady -and $evidenceReady)
$mergeAction = if ($completed) { "READY_FOR_DRY_RUN_APPLY" } else { "HOLD" }
if (-not $inRegister) {
Add-Blocker -Kind "review_return" -Id $reviewId -Severity "P0" -Blocker "复核编号不在专家复核登记表中" -Evidence $reviewId
}
if (-not $resultAllowed) {
Add-Blocker -Kind "review_return" -Id $reviewId -Severity "P0" -Blocker "复核结果枚举非法" -Evidence $reviewResult
}
if ($reviewResult -eq "PENDING") {
Add-Blocker -Kind "review_return" -Id $reviewId -Severity "P0" -Blocker "复核结果仍为 PENDING" -Evidence $reviewId
} elseif (-not $completed) {
Add-Blocker -Kind "review_return" -Id $reviewId -Severity "P0" -Blocker "复核结果缺少复核人、日期或证据路径" -Evidence "reviewer=$($row.reviewer); date=$($row.review_date); evidence=$($row.evidence_path)"
}
$validationRows.Add([pscustomobject][ordered]@{
kind = "review_return"
review_id = $reviewId
asset_id = $row.asset_id
reviewer_role = $row.reviewer_role
reviewer = $row.reviewer
review_result = $reviewResult
in_register = $inRegister
result_allowed = $resultAllowed
reviewer_ready = $reviewerReady
date_ready = $dateReady
evidence_ready = $evidenceReady
completed = $completed
merge_action = $mergeAction
}) | Out-Null
$current = if ($inRegister) { $registerByReviewId[$reviewId] } else { $null }
$diffRows.Add([pscustomobject][ordered]@{
review_id = $reviewId
field = "review_result"
current_value = if ($current) { $current.review_result } else { "" }
returned_value = $reviewResult
merge_action = $mergeAction
}) | Out-Null
$mergeRows.Add([pscustomobject][ordered]@{
review_id = $reviewId
asset_type = $row.asset_type
asset_id = $row.asset_id
merge_action = $mergeAction
next_gate = "apply-phase13-review-results.ps1"
notes = if ($completed) { "可进入 DryRun 回填校验" } else { "等待真实复核结果齐套" }
}) | Out-Null
}
foreach ($ack in $ackReturns) {
$acknowledged = Test-Truthy -Value $ack.acknowledged
$dateReady = -not [string]::IsNullOrWhiteSpace($ack.ack_date)
$recipientReady = -not (Test-PendingName -Value $ack.recipient)
$completed = ($acknowledged -and $dateReady -and $recipientReady)
if (-not $completed) {
Add-Blocker -Kind "ack_return" -Id $ack.work_order_id -Severity "P0" -Blocker "签收回收未完成" -Evidence "acknowledged=$($ack.acknowledged); ack_date=$($ack.ack_date); recipient=$($ack.recipient)"
}
$validationRows.Add([pscustomobject][ordered]@{
kind = "ack_return"
review_id = $ack.work_order_id
asset_id = ""
reviewer_role = $ack.recipient_role
reviewer = $ack.recipient
review_result = if ($acknowledged) { "ACKNOWLEDGED" } else { "PENDING" }
in_register = $true
result_allowed = $true
reviewer_ready = $recipientReady
date_ready = $dateReady
evidence_ready = $true
completed = $completed
merge_action = if ($completed) { "READY_FOR_PROGRESS_GATE" } else { "HOLD" }
}) | Out-Null
}
$validationRows | Export-Csv -LiteralPath $validationBoardCsv -NoTypeInformation -Encoding UTF8
$blockerRows | Export-Csv -LiteralPath $blockersCsv -NoTypeInformation -Encoding UTF8
$diffRows | Export-Csv -LiteralPath $diffPreviewCsv -NoTypeInformation -Encoding UTF8
$mergeRows | Export-Csv -LiteralPath $mergePlanCsv -NoTypeInformation -Encoding UTF8
$completedReviewReturns = @($validationRows | Where-Object { $_.kind -eq "review_return" -and $_.completed }).Count
$pendingReviewReturns = @($validationRows | Where-Object { $_.kind -eq "review_return" -and -not $_.completed }).Count
$completedAckReturns = @($validationRows | Where-Object { $_.kind -eq "ack_return" -and $_.completed }).Count
$pendingAckReturns = @($validationRows | Where-Object { $_.kind -eq "ack_return" -and -not $_.completed }).Count
$mergeReadyRows = @($mergeRows | Where-Object { $_.merge_action -eq "READY_FOR_DRY_RUN_APPLY" }).Count
if ($sourceRows.Count -eq 3 -and @($sourceRows | Where-Object { $_.status -eq "ready" }).Count -eq 3) {
Add-Check -Key "return_files" -Label "回收文件结构" -Status "pass" -Detail "回收文件结构齐套"
} else {
Add-Check -Key "return_files" -Label "回收文件结构" -Status "fail" -Detail "回收文件结构缺失或字段不完整"
}
if ($validationRows.Count -gt 0) {
Add-Check -Key "validation_board" -Label "回收预检看板" -Status "pass" -Detail "已生成 $($validationRows.Count) 条回收预检记录"
} else {
Add-Check -Key "validation_board" -Label "回收预检看板" -Status "fail" -Detail "未生成回收预检记录"
}
if ($completedReviewReturns -gt 0 -and $pendingReviewReturns -eq 0 -and $pendingAckReturns -eq 0) {
Add-Check -Key "return_completion" -Label "真实回收结果齐套" -Status "pass" -Detail "复核与签收回收结果均已齐套"
} else {
Add-Check -Key "return_completion" -Label "真实回收结果齐套" -Status "blocked" -Detail "completed_reviews=$completedReviewReturns; pending_reviews=$pendingReviewReturns; completed_ack=$completedAckReturns; pending_ack=$pendingAckReturns"
}
if ($blockerRows.Count -eq 0) {
Add-Check -Key "return_blockers" -Label "回收预检阻塞项" -Status "pass" -Detail "无回收预检阻塞项"
} else {
Add-Check -Key "return_blockers" -Label "回收预检阻塞项" -Status "blocked" -Detail "回收预检阻塞项=$($blockerRows.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_return_rows = @($validationRows | Where-Object { $_.kind -eq "review_return" -and $_.reviewer_role -eq "法务" }).Count
business_return_rows = @($validationRows | Where-Object { $_.kind -eq "review_return" -and $_.reviewer_role -eq "业务" }).Count
ack_return_rows = @($validationRows | Where-Object { $_.kind -eq "ack_return" }).Count
validation_rows = $validationRows.Count
completed_review_returns = $completedReviewReturns
pending_review_returns = $pendingReviewReturns
completed_ack_returns = $completedAckReturns
pending_ack_returns = $pendingAckReturns
merge_ready_rows = $mergeReadyRows
blocker_count = $blockerRows.Count
}
$summary = [ordered]@{
name = "phase13-review-return-preflight"
status = $overallStatus
generated_at = (Get-Date).ToUniversalTime().ToString("o")
branch = $branch
commit = $commit
remote = $remoteUrl
return_root = $ReturnRoot
review_register_path = $ReviewRegisterPath
checks = $checks.ToArray()
metrics = $metrics
artifacts = [ordered]@{
summary_json = $summaryJson
summary_md = $summaryMd
source_manifest = $sourceManifestCsv
validation_board = $validationBoardCsv
blockers = $blockersCsv
diff_preview = $diffPreviewCsv
merge_plan = $mergePlanCsv
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("- 回收目录:$ReturnRoot") | Out-Null
$md.Add("- 产物目录:$artifactDir") | 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_return_rows) |") | Out-Null
$md.Add("| 业务回收记录 | $($metrics.business_return_rows) |") | Out-Null
$md.Add("| 签收回收记录 | $($metrics.ack_return_rows) |") | Out-Null
$md.Add("| 预检记录 | $($metrics.validation_rows) |") | Out-Null
$md.Add("| 已完成复核回收 | $($metrics.completed_review_returns) |") | Out-Null
$md.Add("| 待完成复核回收 | $($metrics.pending_review_returns) |") | Out-Null
$md.Add("| 待完成签收回收 | $($metrics.pending_ack_returns) |") | Out-Null
$md.Add("| 可进入 DryRun 回填 | $($metrics.merge_ready_rows) |") | Out-Null
$md.Add("| 阻塞项 | $($metrics.blocker_count) |") | 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("回收预检和合并计划已生成,但当前回收模板仍为 PENDING,不能进入真实回填。") | Out-Null
} elseif ($overallStatus -eq "PASS") {
$md.Add("回收结果已通过预检,可进入 DryRun 回填校验。") | Out-Null
} else {
$md.Add("回收预检未完全通过,请查看阻塞项和合并计划。") | Out-Null
}
$md | Set-Content -Encoding UTF8 -Path $summaryMd
Write-Host "PHASE13_REVIEW_RETURN_PREFLIGHT_$overallStatus $artifactDir"
if ($overallStatus -eq "FAIL") {
throw "阶段 13 回收结果预检门禁未通过。查看:$summaryMd"
}
if ($overallStatus -eq "BLOCKED" -and $FailOnBlocked) {
throw "阶段 13 回收结果预检门禁被阻塞。查看:$summaryMd"
}