573 lines
24 KiB
PowerShell
573 lines
24 KiB
PowerShell
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"
|
||
}
|