Files
ai-contract-platform/scripts/run-phase13-review-task-package-gate.ps1
T
2026-05-02 10:27:09 +08:00

745 lines
36 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]$ArtifactRoot = "",
[string]$ReviewPacketPath = "",
[string]$ControlArtifactPath = "",
[string]$ReviewerIntakePath = "",
[string]$LegalReviewer = "",
[string]$BusinessReviewer = "",
[string]$LegalRecipient = "",
[string]$BusinessRecipient = "",
[string]$HandoffOwner = "",
[string]$DueDate = "",
[switch]$FailOnBlocked
)
$ErrorActionPreference = "Stop"
$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
$timestamp = Get-Date -Format "yyyy-MM-ddTHH-mm-ss-fff"
if ([string]::IsNullOrWhiteSpace($ArtifactRoot)) {
$ArtifactRoot = Join-Path $repoRoot "project-docs\qa-artifacts\phase13-review-task-package-gate"
}
if (-not [System.IO.Path]::IsPathRooted($ArtifactRoot)) {
$ArtifactRoot = Join-Path $repoRoot $ArtifactRoot
}
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($ReviewPacketPath)) {
$ReviewPacketPath = Resolve-LatestArtifactDir -RootRelativePath "project-docs\qa-artifacts\phase13-first-wave-review-readiness"
}
if (-not [string]::IsNullOrWhiteSpace($ReviewPacketPath) -and -not [System.IO.Path]::IsPathRooted($ReviewPacketPath)) {
$ReviewPacketPath = Join-Path $repoRoot $ReviewPacketPath
}
if ([string]::IsNullOrWhiteSpace($ControlArtifactPath)) {
$ControlArtifactPath = Resolve-LatestArtifactDir -RootRelativePath "project-docs\qa-artifacts\phase13-review-control-center"
}
if (-not [string]::IsNullOrWhiteSpace($ControlArtifactPath) -and -not [System.IO.Path]::IsPathRooted($ControlArtifactPath)) {
$ControlArtifactPath = Join-Path $repoRoot $ControlArtifactPath
}
if (-not [string]::IsNullOrWhiteSpace($ReviewerIntakePath) -and -not [System.IO.Path]::IsPathRooted($ReviewerIntakePath)) {
$ReviewerIntakePath = Join-Path $repoRoot $ReviewerIntakePath
}
$artifactDir = Join-Path $ArtifactRoot $timestamp
$workOrderRoot = Join-Path $artifactDir "work-orders"
$legalWorkOrderDir = Join-Path $workOrderRoot "legal"
$businessWorkOrderDir = Join-Path $workOrderRoot "business"
$ackWorkOrderDir = Join-Path $workOrderRoot "ack"
New-Item -ItemType Directory -Force -Path $legalWorkOrderDir, $businessWorkOrderDir, $ackWorkOrderDir | Out-Null
$summaryJson = Join-Path $artifactDir "phase13-review-task-package-gate-summary.json"
$summaryMd = Join-Path $artifactDir "phase13-review-task-package-gate-summary.md"
$legalPackageCsv = Join-Path $artifactDir "legal-review-work-orders.csv"
$businessPackageCsv = Join-Path $artifactDir "business-review-work-orders.csv"
$ackPackageCsv = Join-Path $artifactDir "handoff-ack-work-orders.csv"
$acceptanceBoardCsv = Join-Path $artifactDir "review-acceptance-tracking-board.csv"
$dispatchManifestCsv = Join-Path $artifactDir "review-dispatch-manifest.csv"
$operatorGuideMd = Join-Path $artifactDir "review-task-package-operator-guide.md"
$gitStatusPath = Join-Path $artifactDir "git-status.txt"
$checks = New-Object System.Collections.Generic.List[object]
$legalRows = New-Object System.Collections.Generic.List[object]
$businessRows = New-Object System.Collections.Generic.List[object]
$ackRows = New-Object System.Collections.Generic.List[object]
$acceptanceRows = New-Object System.Collections.Generic.List[object]
$dispatchRows = 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 60 | 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-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 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 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-WorkOrderStatus {
param([string]$Assignee, [string]$DueDateValue)
if (Test-PendingValue -Value $Assignee) {
return "PENDING_ASSIGNMENT"
}
if ([string]::IsNullOrWhiteSpace($DueDateValue)) {
return "PENDING_DUE_DATE"
}
return "READY_TO_DISPATCH"
}
function New-ReviewWorkOrder {
param(
[object]$Task,
[ValidateSet("legal_review", "business_review")]
[string]$OrderType,
[string]$Assignee,
[string]$AssigneeRole,
[string]$WorkOrderPath
)
$status = Get-WorkOrderStatus -Assignee $Assignee -DueDateValue $DueDate
$criteria = if ($OrderType -eq "legal_review") {
"法源、条款引用、适用边界、风险等级、修改建议和人工复核提示均经真实法务确认"
} else {
"变量、默认值、枚举、业务适用场景、责任上限和试点可用性均经真实业务负责人确认"
}
$requiredOutput = if ($OrderType -eq "legal_review") {
"在复核结果回填模板中填写法务复核人、复核日期、复核结果、修改要求和依据说明"
} else {
"在复核结果回填模板中填写业务复核人、复核日期、复核结果、修改要求和业务适用说明"
}
return [pscustomobject][ordered]@{
work_order_id = "$($OrderType.ToUpperInvariant())-$($Task.review_id)"
order_type = $OrderType
priority = $Task.priority
review_lane = $Task.review_lane
review_id = $Task.review_id
asset_type = $Task.asset_type
asset_type_text = $Task.asset_type_text
asset_id = $Task.asset_id
asset_name = $Task.asset_name
source_ids = $Task.source_ids
primary_path = $Task.primary_path
secondary_path = $Task.secondary_path
assignee_role = $AssigneeRole
assignee = $Assignee
due_date = $DueDate
current_review_result = $Task.review_result
current_review_status = $Task.review_status
required_action = $Task.required_changes
required_output = $requiredOutput
acceptance_criteria = $criteria
evidence_form_path = $Task.evidence_form_path
work_order_path = $WorkOrderPath
next_gate = "apply-phase13-review-results.ps1 -> run-phase13-review-progress-gate.ps1 -> run-phase13-review-control-center.ps1"
status = $status
notes = "不得由自动化脚本代填 APPROVED;必须由真实复核人签署或留痕后回填"
}
}
function Add-AcceptanceRow {
param([object]$Order, [string]$BlockerArea)
$acceptanceRows.Add([pscustomobject][ordered]@{
acceptance_id = "ACCEPT-$($Order.work_order_id)"
acceptance_type = $Order.order_type
blocker_area = $BlockerArea
review_id = $Order.review_id
asset_id = $Order.asset_id
owner_role = $Order.assignee_role
assignee = $Order.assignee
due_date = $Order.due_date
required_output = $Order.required_output
acceptance_criteria = $Order.acceptance_criteria
evidence_target = $Order.evidence_form_path
current_status = $Order.status
next_gate = $Order.next_gate
}) | Out-Null
}
function Write-ReviewWorkOrderMd {
param([object]$Order, [string]$Path)
$lines = New-Object System.Collections.Generic.List[string]
$roleText = if ($Order.order_type -eq "legal_review") { "法务复核" } else { "业务复核" }
$lines.Add("# 阶段 13 $roleText 任务单:$($Order.review_id)") | Out-Null
$lines.Add("") | Out-Null
$lines.Add("## 任务对象") | Out-Null
$lines.Add("") | Out-Null
$lines.Add("| 字段 | 值 |") | Out-Null
$lines.Add("|---|---|") | Out-Null
$lines.Add("| 工作项 | `$($Order.work_order_id)` |") | Out-Null
$lines.Add("| 资产 | `$($Order.asset_id)`:$($Order.asset_name) |") | Out-Null
$lines.Add("| 类型 | $($Order.asset_type_text) |") | Out-Null
$lines.Add("| 复核人 | $($Order.assignee) |") | Out-Null
$lines.Add("| 截止日期 | $($Order.due_date) |") | Out-Null
$lines.Add("| 主文件 | `$($Order.primary_path)` |") | Out-Null
$lines.Add("| 辅助文件 | `$($Order.secondary_path)` |") | Out-Null
$lines.Add("| 法源或依赖 | `$($Order.source_ids)` |") | Out-Null
$lines.Add("") | Out-Null
$lines.Add("## 执行步骤") | Out-Null
$lines.Add("") | Out-Null
$lines.Add("1. 打开主文件和辅助文件,逐项核对资料内容。") | Out-Null
$lines.Add("2. 按本任务单的验收口径完成复核,不得只给口头结论。") | Out-Null
$lines.Add("3. 在复核结果回填模板中填写复核人、复核日期、复核结果、修改要求和证据路径。") | Out-Null
$lines.Add("4. 若结论为需要修改,必须写明修改位置、修改建议和是否阻断生产发布。") | Out-Null
$lines.Add("5. 回填后运行结果回填、进度、审计归档和总控门禁。") | Out-Null
$lines.Add("") | Out-Null
$lines.Add("## 验收口径") | Out-Null
$lines.Add("") | Out-Null
$lines.Add("- 必做动作:$($Order.required_action)") | Out-Null
$lines.Add("- 回填产物:$($Order.required_output)") | Out-Null
$lines.Add("- 通过标准:$($Order.acceptance_criteria)") | Out-Null
$lines.Add("- 下一门禁:" + '``' + $Order.next_gate + '``') | Out-Null
$lines.Add("") | Out-Null
$lines.Add("## 禁止项") | Out-Null
$lines.Add("") | Out-Null
$lines.Add("1. 不得把 PENDING 直接改成 APPROVED。") | Out-Null
$lines.Add("2. 不得使用脚本自动伪造复核人、复核日期或批准意见。") | Out-Null
$lines.Add("3. 没有真实证据路径时,不得进入生产发布。") | Out-Null
$lines | Set-Content -Encoding UTF8 -Path $Path
}
function Write-AckWorkOrderMd {
param([object]$Order, [string]$Path)
$lines = New-Object System.Collections.Generic.List[string]
$lines.Add("# 阶段 13 交付签收任务单:$($Order.work_order_id)") | Out-Null
$lines.Add("") | Out-Null
$lines.Add("## 签收对象") | Out-Null
$lines.Add("") | Out-Null
$lines.Add("| 字段 | 值 |") | Out-Null
$lines.Add("|---|---|") | Out-Null
$lines.Add("| 签收角色 | $($Order.recipient_role) |") | Out-Null
$lines.Add("| 接收人 | $($Order.recipient) |") | Out-Null
$lines.Add("| 交付负责人 | $($Order.handoff_owner) |") | Out-Null
$lines.Add("| 截止日期 | $($Order.due_date) |") | Out-Null
$lines.Add("| 签收材料 | $($Order.package_path) |") | Out-Null
$lines.Add("") | Out-Null
$lines.Add("## 签收要求") | Out-Null
$lines.Add("") | Out-Null
$lines.Add("1. 接收人确认已收到首批 P0 复核任务包。") | Out-Null
$lines.Add("2. 接收人确认资料可打开、任务数量和角色范围正确。") | Out-Null
$lines.Add("3. 如需补件,必须在签收记录中列明缺失材料。") | Out-Null
$lines.Add("4. 签收记录回填后重新运行复核进度门禁。") | Out-Null
$lines.Add("") | Out-Null
$lines.Add("## 验收标准") | Out-Null
$lines.Add("") | Out-Null
$lines.Add("- `ack_pending=0`。") | Out-Null
$lines.Add("- `acknowledged_rows>0`。") | Out-Null
$lines.Add("- 签收文件中必须包含接收人、签收日期、材料路径和是否需要补件。") | Out-Null
$lines | Set-Content -Encoding UTF8 -Path $Path
}
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 }
}
$selectedTasksPath = if ([string]::IsNullOrWhiteSpace($ReviewPacketPath)) { "" } else { Join-Path $ReviewPacketPath "selected-review-tasks.csv" }
$resultTemplatePath = if ([string]::IsNullOrWhiteSpace($ReviewPacketPath)) { "" } else { Join-Path $ReviewPacketPath "review-result-import-template.csv" }
$assignmentTemplatePath = if ([string]::IsNullOrWhiteSpace($ReviewPacketPath)) { "" } else { Join-Path $ReviewPacketPath "review-assignment-template.csv" }
$controlSummaryPath = if ([string]::IsNullOrWhiteSpace($ControlArtifactPath)) { "" } else { Join-Path $ControlArtifactPath "phase13-review-control-center-summary.json" }
$tasks = Import-CsvRows -Path $selectedTasksPath
$controlSummary = Read-JsonFile -Path $controlSummaryPath
if (-not [string]::IsNullOrWhiteSpace($ReviewerIntakePath) -and (Test-Path -LiteralPath $ReviewerIntakePath -PathType Leaf)) {
$intakeRows = Import-CsvRows -Path $ReviewerIntakePath
foreach ($intakeRow in $intakeRows) {
$roleKey = (Get-FieldValue -Row $intakeRow -Name "role_key").Trim()
$name = (Get-FieldValue -Row $intakeRow -Name "reviewer_name").Trim()
$due = (Get-FieldValue -Row $intakeRow -Name "due_date").Trim()
switch ($roleKey) {
"legal_reviewer" {
if ([string]::IsNullOrWhiteSpace($LegalReviewer)) { $LegalReviewer = $name }
if ([string]::IsNullOrWhiteSpace($DueDate)) { $DueDate = $due }
}
"business_reviewer" {
if ([string]::IsNullOrWhiteSpace($BusinessReviewer)) { $BusinessReviewer = $name }
if ([string]::IsNullOrWhiteSpace($DueDate)) { $DueDate = $due }
}
"legal_recipient" {
if ([string]::IsNullOrWhiteSpace($LegalRecipient)) { $LegalRecipient = $name }
if ([string]::IsNullOrWhiteSpace($DueDate)) { $DueDate = $due }
}
"business_recipient" {
if ([string]::IsNullOrWhiteSpace($BusinessRecipient)) { $BusinessRecipient = $name }
if ([string]::IsNullOrWhiteSpace($DueDate)) { $DueDate = $due }
}
"handoff_owner" {
if ([string]::IsNullOrWhiteSpace($HandoffOwner)) { $HandoffOwner = $name }
if ([string]::IsNullOrWhiteSpace($DueDate)) { $DueDate = $due }
}
}
}
Add-Check -Key "reviewer_intake_file" -Label "真实复核输入文件" -Status "pass" -Required $false -Detail "已读取复核输入文件" -Meta @{ path = $ReviewerIntakePath }
} elseif (-not [string]::IsNullOrWhiteSpace($ReviewerIntakePath)) {
Add-Check -Key "reviewer_intake_file" -Label "真实复核输入文件" -Status "warn" -Required $false -Detail "复核输入文件不存在,继续使用命令行参数" -Meta @{ path = $ReviewerIntakePath }
}
if ($tasks.Count -gt 0) {
Add-Check -Key "review_task_source" -Label "首批复核任务来源" -Status "pass" -Detail "读取到 $($tasks.Count) 个首批复核任务" -Meta @{ source = $selectedTasksPath }
} else {
Add-Check -Key "review_task_source" -Label "首批复核任务来源" -Status "fail" -Detail "未读取到首批复核任务" -Meta @{ source = $selectedTasksPath }
}
if ($null -ne $controlSummary) {
Add-Check -Key "control_center_source" -Label "复核总控来源" -Status "pass" -Detail "读取到总控 blocker 矩阵来源,状态=$($controlSummary.status)" -Meta @{ source = $controlSummaryPath }
} else {
Add-Check -Key "control_center_source" -Label "复核总控来源" -Status "warn" -Required $false -Detail "未读取到总控摘要;仍可基于首批复核任务生成任务包" -Meta @{ source = $controlSummaryPath }
}
$legalAssignee = if ([string]::IsNullOrWhiteSpace($LegalReviewer)) { "待指定真实法务复核人" } else { $LegalReviewer }
$businessAssignee = if ([string]::IsNullOrWhiteSpace($BusinessReviewer)) { "待指定真实业务复核人" } else { $BusinessReviewer }
$handoffAssignee = if ([string]::IsNullOrWhiteSpace($HandoffOwner)) { "待指定交付负责人" } else { $HandoffOwner }
$legalAckRecipient = if ([string]::IsNullOrWhiteSpace($LegalRecipient)) { "待指定法务接收人" } else { $LegalRecipient }
$businessAckRecipient = if ([string]::IsNullOrWhiteSpace($BusinessRecipient)) { "待指定业务接收人" } else { $BusinessRecipient }
foreach ($task in ($tasks | Sort-Object priority, review_lane, asset_type, asset_id)) {
$legalWorkOrderPath = Join-Path $legalWorkOrderDir "$($task.review_id).md"
$legalOrder = New-ReviewWorkOrder -Task $task -OrderType "legal_review" -Assignee $legalAssignee -AssigneeRole "真实法务复核人" -WorkOrderPath $legalWorkOrderPath
$legalRows.Add($legalOrder) | Out-Null
Add-AcceptanceRow -Order $legalOrder -BlockerArea "legal_reviewer_assigned"
Write-ReviewWorkOrderMd -Order $legalOrder -Path $legalWorkOrderPath
if ($task.review_lane -match "业务") {
$businessWorkOrderPath = Join-Path $businessWorkOrderDir "$($task.review_id).md"
$businessOrder = New-ReviewWorkOrder -Task $task -OrderType "business_review" -Assignee $businessAssignee -AssigneeRole "真实业务复核人" -WorkOrderPath $businessWorkOrderPath
$businessRows.Add($businessOrder) | Out-Null
Add-AcceptanceRow -Order $businessOrder -BlockerArea "business_reviewer_assigned"
Write-ReviewWorkOrderMd -Order $businessOrder -Path $businessWorkOrderPath
}
}
$ackSpecs = @(
[pscustomobject]@{ ack_id = "ACK-LEGAL"; recipient_role = "法务接收人"; recipient = $legalAckRecipient; blocker_area = "handoff_acknowledged" },
[pscustomobject]@{ ack_id = "ACK-BUSINESS"; recipient_role = "业务接收人"; recipient = $businessAckRecipient; blocker_area = "handoff_acknowledged" }
)
foreach ($ack in $ackSpecs) {
$ackPath = Join-Path $ackWorkOrderDir "$($ack.ack_id).md"
$ackStatus = Get-WorkOrderStatus -Assignee $ack.recipient -DueDateValue $DueDate
$ackRow = [pscustomobject][ordered]@{
work_order_id = $ack.ack_id
order_type = "handoff_ack"
recipient_role = $ack.recipient_role
recipient = $ack.recipient
handoff_owner = $handoffAssignee
due_date = $DueDate
package_path = $ReviewPacketPath
required_output = "填写并返回接收签收记录,确认收到复核任务包、材料可打开、缺失材料已列明"
acceptance_criteria = "ack_pending=0 且 acknowledged_rows>0"
evidence_target = "recipient-ack-template.csv 或等效签收记录"
work_order_path = $ackPath
next_gate = "run-phase13-review-handoff-gate.ps1 -> run-phase13-review-progress-gate.ps1 -> run-phase13-review-control-center.ps1"
status = $ackStatus
notes = "未签收前不得视为真实复核已启动"
}
$ackRows.Add($ackRow) | Out-Null
$acceptanceRows.Add([pscustomobject][ordered]@{
acceptance_id = "ACCEPT-$($ackRow.work_order_id)"
acceptance_type = $ackRow.order_type
blocker_area = $ack.blocker_area
review_id = ""
asset_id = ""
owner_role = $ackRow.recipient_role
assignee = $ackRow.recipient
due_date = $ackRow.due_date
required_output = $ackRow.required_output
acceptance_criteria = $ackRow.acceptance_criteria
evidence_target = $ackRow.evidence_target
current_status = $ackRow.status
next_gate = $ackRow.next_gate
}) | Out-Null
Write-AckWorkOrderMd -Order $ackRow -Path $ackPath
}
$dispatchRows.Add([pscustomobject][ordered]@{
package = "legal-review-work-orders"
owner = $legalAssignee
rows = $legalRows.Count
artifact = $legalPackageCsv
work_orders = $legalWorkOrderDir
acceptance = "22 项法务复核全部完成并回填"
}) | Out-Null
$dispatchRows.Add([pscustomobject][ordered]@{
package = "business-review-work-orders"
owner = $businessAssignee
rows = $businessRows.Count
artifact = $businessPackageCsv
work_orders = $businessWorkOrderDir
acceptance = "18 项业务复核全部完成并回填"
}) | Out-Null
$dispatchRows.Add([pscustomobject][ordered]@{
package = "handoff-ack-work-orders"
owner = $handoffAssignee
rows = $ackRows.Count
artifact = $ackPackageCsv
work_orders = $ackWorkOrderDir
acceptance = "2 项签收全部完成"
}) | Out-Null
$legalRows | Export-Csv -LiteralPath $legalPackageCsv -NoTypeInformation -Encoding UTF8
$businessRows | Export-Csv -LiteralPath $businessPackageCsv -NoTypeInformation -Encoding UTF8
$ackRows | Export-Csv -LiteralPath $ackPackageCsv -NoTypeInformation -Encoding UTF8
$acceptanceRows | Export-Csv -LiteralPath $acceptanceBoardCsv -NoTypeInformation -Encoding UTF8
$dispatchRows | Export-Csv -LiteralPath $dispatchManifestCsv -NoTypeInformation -Encoding UTF8
$expectedLegalRows = [int](Get-MetricValue -Summary $controlSummary -Name "missing_legal_reviewer_tasks" -Default $tasks.Count)
$expectedBusinessRows = [int](Get-MetricValue -Summary $controlSummary -Name "missing_business_reviewer_tasks" -Default @($tasks | Where-Object { $_.review_lane -match "业务" }).Count)
$expectedAckRows = [int](Get-MetricValue -Summary $controlSummary -Name "ack_pending" -Default 2)
if ($legalRows.Count -eq $expectedLegalRows) {
Add-Check -Key "legal_task_package" -Label "法务复核任务包" -Status "pass" -Detail "已生成 $($legalRows.Count) 项法务复核工作单"
} else {
Add-Check -Key "legal_task_package" -Label "法务复核任务包" -Status "fail" -Detail "法务任务数量不匹配:actual=$($legalRows.Count), expected=$expectedLegalRows"
}
if ($businessRows.Count -eq $expectedBusinessRows) {
Add-Check -Key "business_task_package" -Label "业务复核任务包" -Status "pass" -Detail "已生成 $($businessRows.Count) 项业务复核工作单"
} else {
Add-Check -Key "business_task_package" -Label "业务复核任务包" -Status "fail" -Detail "业务任务数量不匹配:actual=$($businessRows.Count), expected=$expectedBusinessRows"
}
if ($ackRows.Count -eq $expectedAckRows) {
Add-Check -Key "ack_task_package" -Label "签收任务包" -Status "pass" -Detail "已生成 $($ackRows.Count) 项签收工作单"
} else {
Add-Check -Key "ack_task_package" -Label "签收任务包" -Status "fail" -Detail "签收任务数量不匹配:actual=$($ackRows.Count), expected=$expectedAckRows"
}
$expectedAcceptanceRows = $legalRows.Count + $businessRows.Count + $ackRows.Count
if ($acceptanceRows.Count -eq $expectedAcceptanceRows -and $acceptanceRows.Count -gt 0) {
Add-Check -Key "acceptance_tracking_board" -Label "验收追踪看板" -Status "pass" -Detail "已生成 $($acceptanceRows.Count) 个原子验收项"
} else {
Add-Check -Key "acceptance_tracking_board" -Label "验收追踪看板" -Status "fail" -Detail "验收项数量异常:actual=$($acceptanceRows.Count), expected=$expectedAcceptanceRows"
}
$missingLegalAssignee = @($legalRows | Where-Object { Test-PendingValue -Value $_.assignee })
$missingBusinessAssignee = @($businessRows | Where-Object { Test-PendingValue -Value $_.assignee })
$pendingAckRows = @($ackRows | Where-Object { Test-PendingValue -Value $_.recipient })
$missingDueDateRows = @($acceptanceRows | Where-Object { [string]::IsNullOrWhiteSpace($_.due_date) })
$readyOrders = @($acceptanceRows | Where-Object { $_.current_status -eq "READY_TO_DISPATCH" })
$blockedOrders = @($acceptanceRows | Where-Object { $_.current_status -ne "READY_TO_DISPATCH" })
if ($missingLegalAssignee.Count -eq 0) {
Add-Check -Key "legal_assignee_ready" -Label "法务复核人可派发" -Status "pass" -Detail "法务复核工作单均已指定真实复核人"
} else {
Add-Check -Key "legal_assignee_ready" -Label "法务复核人可派发" -Status "blocked" -Detail "仍有 $($missingLegalAssignee.Count) 项法务复核工作单缺少真实复核人"
}
if ($missingBusinessAssignee.Count -eq 0) {
Add-Check -Key "business_assignee_ready" -Label "业务复核人可派发" -Status "pass" -Detail "业务复核工作单均已指定真实复核人"
} else {
Add-Check -Key "business_assignee_ready" -Label "业务复核人可派发" -Status "blocked" -Detail "仍有 $($missingBusinessAssignee.Count) 项业务复核工作单缺少真实复核人"
}
if ($pendingAckRows.Count -eq 0) {
Add-Check -Key "ack_recipient_ready" -Label "签收接收人可派发" -Status "pass" -Detail "签收工作单均已指定接收人"
} else {
Add-Check -Key "ack_recipient_ready" -Label "签收接收人可派发" -Status "blocked" -Detail "仍有 $($pendingAckRows.Count) 项签收工作单缺少真实接收人"
}
if ($missingDueDateRows.Count -eq 0) {
Add-Check -Key "due_date_ready" -Label "复核截止日期可执行" -Status "pass" -Detail "所有验收项均已设置截止日期"
} else {
Add-Check -Key "due_date_ready" -Label "复核截止日期可执行" -Status "blocked" -Detail "仍有 $($missingDueDateRows.Count) 个验收项缺少截止日期"
}
$fabricatedApprovals = @($acceptanceRows | Where-Object { $_.current_status -match "APPROVED|PASS|DONE|COMPLETED" })
if ($fabricatedApprovals.Count -eq 0) {
Add-Check -Key "no_fabricated_review_result" -Label "未伪造复核结论" -Status "pass" -Detail "任务包只生成待执行工作项,没有自动生成批准结论"
} else {
Add-Check -Key "no_fabricated_review_result" -Label "未伪造复核结论" -Status "fail" -Detail "发现疑似自动批准状态:$($fabricatedApprovals.Count)"
}
$guide = New-Object System.Collections.Generic.List[string]
$guide.Add("# 阶段 13 第十五批:复核任务包与验收追踪操作说明") | Out-Null
$guide.Add("") | Out-Null
$guide.Add("本目录把第十四批总控 blocker 拆成可派发、可签收、可回填、可验收的原子工作项。") | Out-Null
$guide.Add("") | Out-Null
$guide.Add("## 包含内容") | Out-Null
$guide.Add("") | Out-Null
$guide.Add('1. `legal-review-work-orders.csv`:22 项法务复核工作单。') | Out-Null
$guide.Add('2. `business-review-work-orders.csv`:18 项业务复核工作单。') | Out-Null
$guide.Add('3. `handoff-ack-work-orders.csv`:2 项签收工作单。') | Out-Null
$guide.Add('4. `review-acceptance-tracking-board.csv`:42 个原子验收项。') | Out-Null
$guide.Add('5. `work-orders/`:逐项 Markdown 任务单。') | Out-Null
$guide.Add("") | Out-Null
$guide.Add("## 执行顺序") | Out-Null
$guide.Add("") | Out-Null
$guide.Add("1. 指定真实法务复核人、真实业务复核人、交付负责人和截止日期。") | Out-Null
$guide.Add('2. 将 `work-orders/ack/` 交给接收人完成签收。') | Out-Null
$guide.Add('3. 将 `work-orders/legal/` 和 `work-orders/business/` 分别交给真实复核人。') | Out-Null
$guide.Add("4. 复核人把结论写入首批复核结果回填模板。") | Out-Null
$guide.Add("5. 依次运行结果回填、进度、审计归档、总控和生产发布门禁。") | Out-Null
$guide.Add("") | Out-Null
$guide.Add("## 推荐命令") | Out-Null
$guide.Add("") | Out-Null
$guide.Add('```powershell') | Out-Null
$guide.Add("powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts\run-phase13-review-task-package-gate.ps1 -LegalReviewer <真实法务姓名> -BusinessReviewer <真实业务姓名> -HandoffOwner <交付负责人> -DueDate <YYYY-MM-DD>") | Out-Null
$guide.Add("powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts\run-phase13-review-handoff-gate.ps1 -LegalReviewer <真实法务姓名> -BusinessReviewer <真实业务姓名> -HandoffOwner <交付负责人> -DueDate <YYYY-MM-DD>") | Out-Null
$guide.Add("powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts\run-phase13-review-progress-gate.ps1") | Out-Null
$guide.Add("powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts\run-phase13-review-control-center.ps1") | Out-Null
$guide.Add('```') | Out-Null
$guide.Add("") | Out-Null
$guide.Add("## 硬性规则") | Out-Null
$guide.Add("") | Out-Null
$guide.Add("1. 没有真实复核人,不得把任务状态视为已启动。") | Out-Null
$guide.Add("2. 没有签收记录,不得把任务状态视为已交付。") | Out-Null
$guide.Add("3. 没有真实复核结论和证据路径,不得把资产状态推进到生产发布。") | Out-Null
$guide | Set-Content -Encoding UTF8 -Path $operatorGuideMd
$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_review_tasks = $tasks.Count
legal_review_work_orders = $legalRows.Count
business_review_work_orders = $businessRows.Count
ack_work_orders = $ackRows.Count
acceptance_tracking_rows = $acceptanceRows.Count
ready_to_dispatch_orders = $readyOrders.Count
blocked_orders = $blockedOrders.Count
missing_legal_assignee_orders = $missingLegalAssignee.Count
missing_business_assignee_orders = $missingBusinessAssignee.Count
ack_pending_orders = $pendingAckRows.Count
missing_due_date_acceptance_items = $missingDueDateRows.Count
expected_legal_review_work_orders = $expectedLegalRows
expected_business_review_work_orders = $expectedBusinessRows
expected_ack_work_orders = $expectedAckRows
control_blocker_count = [int](Get-MetricValue -Summary $controlSummary -Name "blocker_count" -Default 0)
control_readiness_score = [int](Get-MetricValue -Summary $controlSummary -Name "readiness_score" -Default 0)
control_readiness_total = [int](Get-MetricValue -Summary $controlSummary -Name "readiness_total" -Default 0)
}
$summary = [ordered]@{
name = "phase13-review-task-package-gate"
status = $overallStatus
generated_at = (Get-Date).ToUniversalTime().ToString("o")
branch = $branch
commit = $commit
remote = $remoteUrl
review_packet_path = $ReviewPacketPath
control_artifact_path = $ControlArtifactPath
reviewer_intake_path = $ReviewerIntakePath
checks = $checks.ToArray()
metrics = $metrics
artifacts = [ordered]@{
summary_json = $summaryJson
summary_md = $summaryMd
legal_work_orders = $legalPackageCsv
business_work_orders = $businessPackageCsv
ack_work_orders = $ackPackageCsv
acceptance_tracking_board = $acceptanceBoardCsv
dispatch_manifest = $dispatchManifestCsv
operator_guide = $operatorGuideMd
work_orders_dir = $workOrderRoot
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("- 首批复核包:$ReviewPacketPath") | Out-Null
$md.Add("- 总控证据:$ControlArtifactPath") | Out-Null
$md.Add("- 真实复核输入:$ReviewerIntakePath") | 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_review_tasks) |") | Out-Null
$md.Add("| 法务复核工作单 | $($metrics.legal_review_work_orders) |") | Out-Null
$md.Add("| 业务复核工作单 | $($metrics.business_review_work_orders) |") | Out-Null
$md.Add("| 签收工作单 | $($metrics.ack_work_orders) |") | Out-Null
$md.Add("| 原子验收项 | $($metrics.acceptance_tracking_rows) |") | Out-Null
$md.Add("| 可派发工作项 | $($metrics.ready_to_dispatch_orders) |") | Out-Null
$md.Add("| 阻塞工作项 | $($metrics.blocked_orders) |") | 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("第十五批已把总控 blocker 拆成可执行任务包,但仍缺少真实复核人、签收接收人或截止日期,因此只能进入派发准备状态,不能视为真实复核完成。") | 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-task-package-gate-summary.json") | Out-Null
$md.Add("- phase13-review-task-package-gate-summary.md") | Out-Null
$md.Add("- legal-review-work-orders.csv") | Out-Null
$md.Add("- business-review-work-orders.csv") | Out-Null
$md.Add("- handoff-ack-work-orders.csv") | Out-Null
$md.Add("- review-acceptance-tracking-board.csv") | Out-Null
$md.Add("- review-dispatch-manifest.csv") | Out-Null
$md.Add("- review-task-package-operator-guide.md") | Out-Null
$md.Add("- work-orders/") | Out-Null
$md | Set-Content -Encoding UTF8 -Path $summaryMd
Write-Host "PHASE13_REVIEW_TASK_PACKAGE_GATE_$overallStatus $artifactDir"
if ($overallStatus -eq "FAIL") {
throw "阶段 13 复核任务包与验收追踪门禁未通过。查看:$summaryMd"
}
if ($overallStatus -eq "BLOCKED" -and $FailOnBlocked) {
throw "阶段 13 复核任务包与验收追踪门禁被阻塞。查看:$summaryMd"
}