745 lines
36 KiB
PowerShell
745 lines
36 KiB
PowerShell
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"
|
||
}
|