Files
ai-contract-platform/scripts/run-phase13-review-progress-gate.ps1
T
2026-05-02 00:01:44 +08:00

573 lines
24 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]$ReviewHandoffPath = "",
[string]$ReviewResultsPath = "",
[string]$RecipientAckPath = "",
[string]$ArtifactRoot = "",
[string]$DueDate = "",
[int]$WarningDays = 3,
[switch]$FailOnBlocked
)
$ErrorActionPreference = "Stop"
$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
$timestamp = Get-Date -Format "yyyy-MM-ddTHH-mm-ss-fff"
if ([string]::IsNullOrWhiteSpace($ReviewHandoffPath)) {
$handoffRoot = Join-Path $repoRoot "project-docs\qa-artifacts\phase13-review-handoff-gate"
$latestHandoff = Get-ChildItem -LiteralPath $handoffRoot -Directory -ErrorAction SilentlyContinue |
Sort-Object Name -Descending |
Select-Object -First 1
if ($latestHandoff) {
$ReviewHandoffPath = $latestHandoff.FullName
}
}
if (-not [string]::IsNullOrWhiteSpace($ReviewHandoffPath) -and -not [System.IO.Path]::IsPathRooted($ReviewHandoffPath)) {
$ReviewHandoffPath = Join-Path $repoRoot $ReviewHandoffPath
}
if ([string]::IsNullOrWhiteSpace($ArtifactRoot)) {
$ArtifactRoot = Join-Path $repoRoot "project-docs\qa-artifacts\phase13-review-progress-gate"
}
if (-not [System.IO.Path]::IsPathRooted($ArtifactRoot)) {
$ArtifactRoot = Join-Path $repoRoot $ArtifactRoot
}
$handoffPackagePath = Join-Path $ReviewHandoffPath "handoff-package"
$materialsPath = Join-Path $handoffPackagePath "materials"
if ([string]::IsNullOrWhiteSpace($ReviewResultsPath)) {
$ReviewResultsPath = Join-Path $materialsPath "review-result-import-template.csv"
}
if (-not [string]::IsNullOrWhiteSpace($ReviewResultsPath) -and -not [System.IO.Path]::IsPathRooted($ReviewResultsPath)) {
$ReviewResultsPath = Join-Path $repoRoot $ReviewResultsPath
}
if ([string]::IsNullOrWhiteSpace($RecipientAckPath)) {
$RecipientAckPath = Join-Path $handoffPackagePath "recipient-ack-template.csv"
}
if (-not [string]::IsNullOrWhiteSpace($RecipientAckPath) -and -not [System.IO.Path]::IsPathRooted($RecipientAckPath)) {
$RecipientAckPath = Join-Path $repoRoot $RecipientAckPath
}
$artifactDir = Join-Path $ArtifactRoot $timestamp
$reminderDir = Join-Path $artifactDir "reminder-messages"
New-Item -ItemType Directory -Force -Path $reminderDir | Out-Null
$summaryJson = Join-Path $artifactDir "phase13-review-progress-gate-summary.json"
$summaryMd = Join-Path $artifactDir "phase13-review-progress-gate-summary.md"
$progressBoardCsv = Join-Path $artifactDir "review-progress-board.csv"
$reminderQueueCsv = Join-Path $artifactDir "review-reminder-queue.csv"
$escalationBlockersCsv = Join-Path $artifactDir "review-progress-escalation-blockers.csv"
$gitStatusPath = Join-Path $artifactDir "git-status.txt"
$checks = New-Object System.Collections.Generic.List[object]
$progressRows = New-Object System.Collections.Generic.List[object]
$reminderRows = New-Object System.Collections.Generic.List[object]
$blockerRows = 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 Add-Blocker {
param([string]$Scope, [string]$Key, [string]$Detail)
$blockerRows.Add([pscustomobject][ordered]@{
scope = $Scope
key = $Key
detail = $Detail
}) | Out-Null
}
function Add-Reminder {
param(
[string]$ReminderType,
[string]$TargetRole,
[string]$TargetName,
[string]$ReviewId,
[string]$AssetId,
[string]$Reason,
[string]$DueDateText
)
$reminderRows.Add([pscustomobject][ordered]@{
reminder_type = $ReminderType
target_role = $TargetRole
target_name = $TargetName
review_id = $ReviewId
asset_id = $AssetId
reason = $Reason
due_date = $DueDateText
}) | Out-Null
}
function Save-Json {
param([string]$Path, [object]$Value)
$Value | ConvertTo-Json -Depth 40 | 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-FieldValue {
param([object]$Row, [string]$Name)
if ($null -eq $Row) {
return ""
}
$property = $Row.PSObject.Properties[$Name]
if ($null -eq $property -or $null -eq $property.Value) {
return ""
}
return [string]$property.Value
}
function Test-PendingValue {
param([string]$Value)
if ([string]::IsNullOrWhiteSpace($Value)) {
return $true
}
return @("pending", "待法务复核", "待业务复核", "待法务和业务复核", "待复核", "待确认", "tbd") -contains $Value.Trim().ToLowerInvariant()
}
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 Get-ReviewProgressStatus {
param([string]$ReviewResult, [string]$ReviewStatus)
if ($ReviewResult -eq "REJECTED" -or $ReviewStatus -eq "REJECTED") {
return "rejected"
}
if ($ReviewResult -eq "CHANGES_REQUIRED" -or $ReviewStatus -eq "CHANGES_REQUIRED") {
return "changes_required"
}
if (@("APPROVED_FOR_REVIEW_IMPORT", "APPROVED_FOR_PRODUCTION") -contains $ReviewResult -and $ReviewStatus -eq "COMPLETED") {
return "completed"
}
if ($ReviewStatus -eq "IN_PROGRESS") {
return "in_progress"
}
return "pending"
}
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 (-not [string]::IsNullOrWhiteSpace($ReviewHandoffPath) -and (Test-Path -LiteralPath $ReviewHandoffPath -PathType Container)) {
Add-Check -Key "review_handoff_source" -Label "复核交付包来源" -Status "pass" -Detail "已找到复核交付包" -Meta @{ path = $ReviewHandoffPath }
} else {
Add-Check -Key "review_handoff_source" -Label "复核交付包来源" -Status "fail" -Detail "未找到复核交付包" -Meta @{ path = $ReviewHandoffPath }
}
if (Test-Path -LiteralPath $ReviewResultsPath -PathType Leaf) {
Add-Check -Key "review_results_file" -Label "复核结果文件" -Status "pass" -Detail "复核结果文件存在" -Meta @{ path = $ReviewResultsPath }
} else {
Add-Check -Key "review_results_file" -Label "复核结果文件" -Status "fail" -Detail "复核结果文件不存在" -Meta @{ path = $ReviewResultsPath }
}
if (Test-Path -LiteralPath $RecipientAckPath -PathType Leaf) {
Add-Check -Key "recipient_ack_file" -Label "签收文件" -Status "pass" -Detail "签收文件存在" -Meta @{ path = $RecipientAckPath }
} else {
Add-Check -Key "recipient_ack_file" -Label "签收文件" -Status "blocked" -Detail "签收文件不存在" -Meta @{ path = $RecipientAckPath }
Add-Blocker -Scope "ack" -Key "missing_ack_file" -Detail "签收文件不存在"
}
$selectedTasksPath = Join-Path $materialsPath "selected-review-tasks.csv"
$selectedTasks = Import-CsvRows -Path $selectedTasksPath
$resultRows = Import-CsvRows -Path $ReviewResultsPath
$ackRows = Import-CsvRows -Path $RecipientAckPath
if ($selectedTasks.Count -gt 0) {
Add-Check -Key "selected_tasks" -Label "复核任务清单" -Status "pass" -Detail "复核任务数量=$($selectedTasks.Count)"
} else {
Add-Check -Key "selected_tasks" -Label "复核任务清单" -Status "fail" -Detail "复核任务清单为空"
}
if ($resultRows.Count -eq $selectedTasks.Count -and $resultRows.Count -gt 0) {
Add-Check -Key "result_task_alignment" -Label "复核结果任务对齐" -Status "pass" -Detail "复核结果行数与任务数一致"
} else {
Add-Check -Key "result_task_alignment" -Label "复核结果任务对齐" -Status "blocked" -Detail "复核结果行数=$($resultRows.Count),任务数=$($selectedTasks.Count)"
Add-Blocker -Scope "results" -Key "result_task_mismatch" -Detail "复核结果行数与任务数不一致"
}
$dueDateText = $DueDate
if ([string]::IsNullOrWhiteSpace($dueDateText) -and $selectedTasks.Count -gt 0) {
$dueDateText = Get-FieldValue -Row $selectedTasks[0] -Name "due_date"
}
$dueDateMissing = [string]::IsNullOrWhiteSpace($dueDateText)
$dueDateInvalid = $false
$dueDatePast = $false
$dueSoon = $false
$daysUntilDue = $null
if (-not $dueDateMissing) {
$parsedDueDate = [datetime]::MinValue
$dueDateInvalid = -not [datetime]::TryParse($dueDateText, [ref]$parsedDueDate)
if (-not $dueDateInvalid) {
$today = (Get-Date).Date
$daysUntilDue = [int]($parsedDueDate.Date - $today).TotalDays
$dueDatePast = $daysUntilDue -lt 0
$dueSoon = $daysUntilDue -ge 0 -and $daysUntilDue -le $WarningDays
}
}
if ($dueDateMissing) {
Add-Check -Key "due_date" -Label "复核截止日期" -Status "blocked" -Detail "缺少复核截止日期"
Add-Blocker -Scope "sla" -Key "missing_due_date" -Detail "缺少复核截止日期"
} elseif ($dueDateInvalid) {
Add-Check -Key "due_date" -Label "复核截止日期" -Status "fail" -Detail "复核截止日期无法解析:$dueDateText"
Add-Blocker -Scope "sla" -Key "invalid_due_date" -Detail "复核截止日期无法解析:$dueDateText"
} elseif ($dueDatePast) {
Add-Check -Key "due_date" -Label "复核截止日期" -Status "blocked" -Detail "复核截止日期已逾期:$dueDateText"
Add-Blocker -Scope "sla" -Key "overdue_due_date" -Detail "复核截止日期已逾期:$dueDateText"
} elseif ($dueSoon) {
Add-Check -Key "due_date" -Label "复核截止日期" -Status "warn" -Detail "复核截止日期临近:$dueDateText,剩余 $daysUntilDue 天"
} else {
Add-Check -Key "due_date" -Label "复核截止日期" -Status "pass" -Detail "复核截止日期=$dueDateText,剩余 $daysUntilDue 天"
}
$ackPendingRows = @()
$ackCompleteRows = @()
foreach ($row in $ackRows) {
$ackRole = Get-FieldValue -Row $row -Name "ack_role"
$recipientName = Get-FieldValue -Row $row -Name "recipient_name"
$ackStatus = Get-FieldValue -Row $row -Name "ack_status"
$ackDate = Get-FieldValue -Row $row -Name "ack_date"
$isComplete = -not (Test-PendingValue -Value $recipientName) -and $ackStatus -eq "ACKNOWLEDGED" -and -not [string]::IsNullOrWhiteSpace($ackDate)
if ($isComplete) {
$ackCompleteRows += $row
} else {
$ackPendingRows += $row
Add-Reminder -ReminderType "ack" -TargetRole $ackRole -TargetName $recipientName -ReviewId "" -AssetId "" -Reason "接收签收未完成" -DueDateText $dueDateText
}
}
if ($ackRows.Count -eq 0) {
Add-Check -Key "ack_progress" -Label "接收签收进度" -Status "blocked" -Detail "没有接收签收记录"
Add-Blocker -Scope "ack" -Key "ack_empty" -Detail "没有接收签收记录"
} elseif ($ackPendingRows.Count -eq 0) {
Add-Check -Key "ack_progress" -Label "接收签收进度" -Status "pass" -Detail "接收签收已完成"
} else {
Add-Check -Key "ack_progress" -Label "接收签收进度" -Status "blocked" -Detail "仍有 $($ackPendingRows.Count) 条签收未完成"
Add-Blocker -Scope "ack" -Key "ack_pending" -Detail "仍有 $($ackPendingRows.Count) 条签收未完成"
}
$resultMap = @{}
foreach ($row in $resultRows) {
$reviewId = Get-FieldValue -Row $row -Name "review_id"
if (-not [string]::IsNullOrWhiteSpace($reviewId)) {
$resultMap[$reviewId] = $row
}
}
$pendingReviews = 0
$inProgressReviews = 0
$changesRequiredReviews = 0
$completedReviews = 0
$rejectedReviews = 0
$missingResultRows = 0
foreach ($task in $selectedTasks) {
$reviewId = Get-FieldValue -Row $task -Name "review_id"
$assetId = Get-FieldValue -Row $task -Name "asset_id"
$assetType = Get-FieldValue -Row $task -Name "asset_type"
$assetName = Get-FieldValue -Row $task -Name "asset_name"
$priority = Get-FieldValue -Row $task -Name "priority"
$reviewLane = Get-FieldValue -Row $task -Name "review_lane"
$resultRow = if ($resultMap.ContainsKey($reviewId)) { $resultMap[$reviewId] } else { $null }
if ($null -eq $resultRow) {
$missingResultRows += 1
$progressStatus = "missing_result"
$reviewResult = ""
$reviewStatus = ""
$reviewer = ""
$reviewDate = ""
Add-Blocker -Scope "review" -Key "missing_result:$reviewId" -Detail "缺少复核结果行"
Add-Reminder -ReminderType "review_result_missing" -TargetRole $reviewLane -TargetName "" -ReviewId $reviewId -AssetId $assetId -Reason "缺少复核结果行" -DueDateText $dueDateText
} else {
$reviewResult = Get-FieldValue -Row $resultRow -Name "review_result"
$reviewStatus = Get-FieldValue -Row $resultRow -Name "status"
$reviewer = Get-FieldValue -Row $resultRow -Name "reviewer"
$reviewDate = Get-FieldValue -Row $resultRow -Name "review_date"
$progressStatus = Get-ReviewProgressStatus -ReviewResult $reviewResult -ReviewStatus $reviewStatus
}
switch ($progressStatus) {
"pending" {
$pendingReviews += 1
Add-Blocker -Scope "review" -Key "pending:$reviewId" -Detail "复核任务仍为 PENDING"
Add-Reminder -ReminderType "review_pending" -TargetRole $reviewLane -TargetName $reviewer -ReviewId $reviewId -AssetId $assetId -Reason "复核任务仍为 PENDING" -DueDateText $dueDateText
}
"in_progress" { $inProgressReviews += 1 }
"changes_required" { $changesRequiredReviews += 1 }
"completed" { $completedReviews += 1 }
"rejected" { $rejectedReviews += 1 }
}
$progressRows.Add([pscustomobject][ordered]@{
priority = $priority
review_lane = $reviewLane
review_id = $reviewId
asset_type = $assetType
asset_id = $assetId
asset_name = $assetName
review_result = $reviewResult
review_status = $reviewStatus
progress_status = $progressStatus
reviewer = $reviewer
review_date = $reviewDate
due_date = $dueDateText
days_until_due = $daysUntilDue
ack_completed = ($ackPendingRows.Count -eq 0 -and $ackRows.Count -gt 0)
}) | Out-Null
}
if ($pendingReviews -eq 0 -and $missingResultRows -eq 0 -and $selectedTasks.Count -gt 0) {
Add-Check -Key "review_progress" -Label "复核任务进度" -Status "pass" -Detail "首批复核任务均已脱离 PENDING"
} else {
Add-Check -Key "review_progress" -Label "复核任务进度" -Status "blocked" -Detail "仍有 $pendingReviews 个 PENDING 任务,$missingResultRows 个缺少结果行"
}
if ($completedReviews -gt 0 -or $changesRequiredReviews -gt 0 -or $rejectedReviews -gt 0) {
Add-Check -Key "review_activity" -Label "复核活动记录" -Status "pass" -Detail "已有复核活动记录"
} else {
Add-Check -Key "review_activity" -Label "复核活动记录" -Status "blocked" -Detail "尚未发现真实复核活动记录"
Add-Blocker -Scope "review" -Key "no_review_activity" -Detail "尚未发现真实复核活动记录"
}
$progressRows |
Sort-Object priority, review_lane, asset_type, asset_id |
Export-Csv -LiteralPath $progressBoardCsv -NoTypeInformation -Encoding UTF8
$reminderRows |
Sort-Object reminder_type, target_role, review_id |
Export-Csv -LiteralPath $reminderQueueCsv -NoTypeInformation -Encoding UTF8
$blockerRows |
Sort-Object scope, key |
Export-Csv -LiteralPath $escalationBlockersCsv -NoTypeInformation -Encoding UTF8
$legalReminderPath = Join-Path $reminderDir "legal-review-reminder.md"
$businessReminderPath = Join-Path $reminderDir "business-review-reminder.md"
$ownerEscalationPath = Join-Path $reminderDir "handoff-owner-escalation.md"
$legalReminder = New-Object System.Collections.Generic.List[string]
$legalReminder.Add("# 法务复核催办") | Out-Null
$legalReminder.Add("") | Out-Null
$legalReminder.Add("首批 P0 法务复核仍未完成。请确认接收签收、复核意见、复核日期、复核结果和证据路径。") | Out-Null
$legalReminder.Add("") | Out-Null
$legalReminder.Add("- 复核任务总数:$($selectedTasks.Count)") | Out-Null
$legalReminder.Add("- PENDING 任务:$pendingReviews") | Out-Null
$legalReminder.Add("- 截止日期:$dueDateText") | Out-Null
$legalReminder.Add("- 进度看板:$progressBoardCsv") | Out-Null
$legalReminder | Set-Content -Encoding UTF8 -Path $legalReminderPath
$businessReminder = New-Object System.Collections.Generic.List[string]
$businessReminder.Add("# 业务复核催办") | Out-Null
$businessReminder.Add("") | Out-Null
$businessReminder.Add("首批 P0 中涉及模板、条款和生成样本的业务复核仍未完成。请确认变量、默认值、适用场景、业务风险和证据路径。") | Out-Null
$businessReminder.Add("") | Out-Null
$businessReminder.Add("- 法务+业务任务:$(@($selectedTasks | Where-Object { $_.review_lane -eq '法务+业务' }).Count)") | Out-Null
$businessReminder.Add("- PENDING 任务:$pendingReviews") | Out-Null
$businessReminder.Add("- 截止日期:$dueDateText") | Out-Null
$businessReminder.Add("- 进度看板:$progressBoardCsv") | Out-Null
$businessReminder | Set-Content -Encoding UTF8 -Path $businessReminderPath
$ownerEscalation = New-Object System.Collections.Generic.List[string]
$ownerEscalation.Add("# 复核交付负责人升级提醒") | Out-Null
$ownerEscalation.Add("") | Out-Null
$ownerEscalation.Add("复核执行仍处于阻塞状态。请补齐真实签收、截止日期和复核结果,否则生产发布门禁会继续阻断。") | Out-Null
$ownerEscalation.Add("") | Out-Null
$ownerEscalation.Add("- 阻塞项:$($blockerRows.Count)") | Out-Null
$ownerEscalation.Add("- 催办队列:$($reminderRows.Count)") | Out-Null
$ownerEscalation.Add("- 阻塞清单:$escalationBlockersCsv") | Out-Null
$ownerEscalation | Set-Content -Encoding UTF8 -Path $ownerEscalationPath
$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]@{
selected_tasks = $selectedTasks.Count
result_rows = $resultRows.Count
ack_rows = $ackRows.Count
ack_pending = $ackPendingRows.Count
ack_completed = $ackCompleteRows.Count
pending_reviews = $pendingReviews
in_progress_reviews = $inProgressReviews
changes_required_reviews = $changesRequiredReviews
completed_reviews = $completedReviews
rejected_reviews = $rejectedReviews
missing_result_rows = $missingResultRows
reminder_queue_rows = $reminderRows.Count
escalation_blockers = $blockerRows.Count
due_date_missing = $dueDateMissing
due_date_invalid = $dueDateInvalid
due_date_past = $dueDatePast
due_date_soon = $dueSoon
days_until_due = $daysUntilDue
}
$summary = [ordered]@{
name = "phase13-review-progress-gate"
status = $overallStatus
generated_at = (Get-Date).ToUniversalTime().ToString("o")
branch = $branch
commit = $commit
remote = $remoteUrl
review_handoff_path = $ReviewHandoffPath
review_results_path = $ReviewResultsPath
recipient_ack_path = $RecipientAckPath
checks = $checks.ToArray()
metrics = $metrics
artifacts = [ordered]@{
summary_json = $summaryJson
summary_md = $summaryMd
progress_board = $progressBoardCsv
reminder_queue = $reminderQueueCsv
escalation_blockers = $escalationBlockersCsv
reminder_messages = $reminderDir
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("- 来源交付包:$ReviewHandoffPath") | Out-Null
$md.Add("- 复核结果文件:$ReviewResultsPath") | Out-Null
$md.Add("- 签收文件:$RecipientAckPath") | 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.selected_tasks) |") | Out-Null
$md.Add("| 复核结果行 | $($metrics.result_rows) |") | Out-Null
$md.Add("| 签收记录 | $($metrics.ack_rows) |") | Out-Null
$md.Add("| 未完成签收 | $($metrics.ack_pending) |") | Out-Null
$md.Add("| PENDING 复核 | $($metrics.pending_reviews) |") | Out-Null
$md.Add("| 进行中复核 | $($metrics.in_progress_reviews) |") | Out-Null
$md.Add("| 需修改复核 | $($metrics.changes_required_reviews) |") | Out-Null
$md.Add("| 已完成复核 | $($metrics.completed_reviews) |") | Out-Null
$md.Add("| 已拒绝复核 | $($metrics.rejected_reviews) |") | Out-Null
$md.Add("| 催办队列 | $($metrics.reminder_queue_rows) |") | Out-Null
$md.Add("| 升级阻塞项 | $($metrics.escalation_blockers) |") | 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.Add("") | Out-Null
$md.Add("本地产物:") | Out-Null
$md.Add("") | Out-Null
$md.Add("- phase13-review-progress-gate-summary.json") | Out-Null
$md.Add("- phase13-review-progress-gate-summary.md") | Out-Null
$md.Add("- review-progress-board.csv") | Out-Null
$md.Add("- review-reminder-queue.csv") | Out-Null
$md.Add("- review-progress-escalation-blockers.csv") | Out-Null
$md.Add("- reminder-messages/") | Out-Null
$md | Set-Content -Encoding UTF8 -Path $summaryMd
Write-Host "PHASE13_REVIEW_PROGRESS_GATE_$overallStatus $artifactDir"
if ($overallStatus -eq "FAIL") {
throw "阶段 13 复核进度与催办门禁未通过。查看:$summaryMd"
}
if ($overallStatus -eq "BLOCKED" -and $FailOnBlocked) {
throw "阶段 13 复核进度与催办门禁被阻塞。查看:$summaryMd"
}