558 lines
25 KiB
PowerShell
558 lines
25 KiB
PowerShell
param(
|
||
[string]$LibraryRoot = "",
|
||
[string]$ArtifactRoot = "",
|
||
[int]$MinReviewRegistrations = 1,
|
||
[switch]$FailOnBlocked
|
||
)
|
||
|
||
$ErrorActionPreference = "Stop"
|
||
|
||
$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
|
||
$timestamp = Get-Date -Format "yyyy-MM-ddTHH-mm-ss-fff"
|
||
|
||
if ([string]::IsNullOrWhiteSpace($LibraryRoot)) {
|
||
$LibraryRoot = Join-Path $repoRoot "project-docs\phase13-legal-knowledge-template-library"
|
||
}
|
||
if (-not [System.IO.Path]::IsPathRooted($LibraryRoot)) {
|
||
$LibraryRoot = Join-Path $repoRoot $LibraryRoot
|
||
}
|
||
|
||
if ([string]::IsNullOrWhiteSpace($ArtifactRoot)) {
|
||
$ArtifactRoot = Join-Path $repoRoot "project-docs\qa-artifacts\phase13-review-execution-gate"
|
||
}
|
||
if (-not [System.IO.Path]::IsPathRooted($ArtifactRoot)) {
|
||
$ArtifactRoot = Join-Path $repoRoot $ArtifactRoot
|
||
}
|
||
|
||
$artifactDir = Join-Path $ArtifactRoot $timestamp
|
||
New-Item -ItemType Directory -Force -Path $artifactDir | Out-Null
|
||
|
||
$summaryJson = Join-Path $artifactDir "phase13-review-execution-gate-summary.json"
|
||
$summaryMd = Join-Path $artifactDir "phase13-review-execution-gate-summary.md"
|
||
$taskBoardCsv = Join-Path $artifactDir "review-task-board.csv"
|
||
$batchSummaryCsv = Join-Path $artifactDir "review-batch-summary.csv"
|
||
$blockersCsv = Join-Path $artifactDir "review-execution-blockers.csv"
|
||
$gitStatusPath = Join-Path $artifactDir "git-status.txt"
|
||
|
||
$checks = 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 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 Split-SemicolonValues {
|
||
param([string]$Value)
|
||
if ([string]::IsNullOrWhiteSpace($Value)) {
|
||
return @()
|
||
}
|
||
return @($Value -split ";" | ForEach-Object { $_.Trim() } | Where-Object { $_ })
|
||
}
|
||
|
||
function Test-RequiredFlag {
|
||
param([string]$Value)
|
||
if ([string]::IsNullOrWhiteSpace($Value)) {
|
||
return $false
|
||
}
|
||
return @("true", "yes", "1", "required") -contains $Value.ToLowerInvariant()
|
||
}
|
||
|
||
function Test-PendingReviewer {
|
||
param([string]$Value)
|
||
if ([string]::IsNullOrWhiteSpace($Value)) {
|
||
return $true
|
||
}
|
||
return @("pending", "待法务复核", "待业务复核", "待法务和业务复核", "待复核") -contains $Value.Trim()
|
||
}
|
||
|
||
function Test-LibraryFilePath {
|
||
param([string]$RelativePath)
|
||
if ([string]::IsNullOrWhiteSpace($RelativePath)) {
|
||
return $false
|
||
}
|
||
return Test-Path -LiteralPath (Join-Path $LibraryRoot ($RelativePath -replace "/", "\")) -PathType Leaf
|
||
}
|
||
|
||
function Convert-StatusText {
|
||
param([string]$Status)
|
||
switch ($Status) {
|
||
"PASS" { return "通过" }
|
||
"FAIL" { return "未通过" }
|
||
"BLOCKED" { return "阻塞" }
|
||
"WARN" { 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-AssetTypeText {
|
||
param([string]$AssetType)
|
||
switch ($AssetType) {
|
||
"legal_source" { return "法源" }
|
||
"knowledge_card" { return "知识卡" }
|
||
"review_rule" { return "审核规则" }
|
||
"clause" { return "标准条款" }
|
||
"contract_template" { return "合同模板" }
|
||
"review_golden_sample" { return "审核黄金样本" }
|
||
"generation_golden_sample" { return "生成黄金样本" }
|
||
"kb_qa_golden_sample" { return "知识库问答样本" }
|
||
default { return $AssetType }
|
||
}
|
||
}
|
||
|
||
function Get-ReviewLane {
|
||
param([string]$AssetType, [string]$ReviewerRole)
|
||
if ($ReviewerRole -match "业务") {
|
||
return "法务+业务"
|
||
}
|
||
if ($AssetType -eq "contract_template" -or $AssetType -eq "clause" -or $AssetType -eq "generation_golden_sample") {
|
||
return "法务+业务"
|
||
}
|
||
return "法务"
|
||
}
|
||
|
||
function Get-ReviewPriority {
|
||
param([string]$AssetType, [object]$Asset)
|
||
if ($AssetType -eq "contract_template") {
|
||
return "P0"
|
||
}
|
||
if ($AssetType -eq "generation_golden_sample" -and $Asset.asset_id -match "GEN-SAMPLE-TPL-") {
|
||
return "P0"
|
||
}
|
||
if ($AssetType -eq "review_rule" -and $Asset.severity -eq "high") {
|
||
return "P0"
|
||
}
|
||
if ($AssetType -eq "clause") {
|
||
return "P0"
|
||
}
|
||
if ($AssetType -eq "legal_source") {
|
||
return "P1"
|
||
}
|
||
if ($AssetType -eq "knowledge_card" -or $AssetType -eq "review_rule" -or $AssetType -eq "review_golden_sample") {
|
||
return "P1"
|
||
}
|
||
return "P2"
|
||
}
|
||
|
||
function New-AssetMeta {
|
||
param(
|
||
[string]$AssetType,
|
||
[string]$AssetId,
|
||
[string]$AssetName,
|
||
[string]$Status,
|
||
[string]$ProductionReady,
|
||
[string]$LegalReviewer,
|
||
[string]$BusinessReviewer,
|
||
[string]$Severity = "",
|
||
[string]$SourceIds = ""
|
||
)
|
||
return [pscustomobject][ordered]@{
|
||
asset_type = $AssetType
|
||
asset_id = $AssetId
|
||
asset_name = $AssetName
|
||
status = $Status
|
||
production_ready = $ProductionReady
|
||
legal_reviewer = $LegalReviewer
|
||
business_reviewer = $BusinessReviewer
|
||
severity = $Severity
|
||
source_ids = $SourceIds
|
||
}
|
||
}
|
||
|
||
if (-not (Test-Path "$HOME\.Codex\skills\gstack\bin")) {
|
||
Add-Check -Key "gstack" -Label "gstack 前置检查" -Status "fail" -Detail "gstack 缺失"
|
||
} else {
|
||
Add-Check -Key "gstack" -Label "gstack 前置检查" -Status "pass" -Detail "GSTACK_OK"
|
||
}
|
||
|
||
$branch = (git -C $repoRoot rev-parse --abbrev-ref HEAD).Trim()
|
||
$commit = (git -C $repoRoot rev-parse HEAD).Trim()
|
||
$remoteUrl = (git -C $repoRoot remote get-url origin).Trim()
|
||
$gitStatus = git -C $repoRoot status --short
|
||
($gitStatus -join [Environment]::NewLine) | Set-Content -Encoding UTF8 -Path $gitStatusPath
|
||
|
||
if ($remoteUrl -match "192\.168\.3\.90") {
|
||
Add-Check -Key "origin_remote" -Label "origin 远端仓库" -Status "pass" -Detail "origin 指向 192.168.3.90" -Meta @{ branch = $branch; commit = $commit; remote = $remoteUrl }
|
||
} else {
|
||
Add-Check -Key "origin_remote" -Label "origin 远端仓库" -Status "fail" -Detail "origin 未指向 192.168.3.90" -Meta @{ branch = $branch; commit = $commit; remote = $remoteUrl }
|
||
}
|
||
|
||
if ($gitStatus) {
|
||
Add-Check -Key "working_tree" -Label "工作区状态" -Status "warn" -Required $false -Detail "工作区存在未提交改动;本次门禁按证据记录" -Meta @{ artifact = $gitStatusPath }
|
||
} else {
|
||
Add-Check -Key "working_tree" -Label "工作区状态" -Status "pass" -Detail "工作区干净" -Meta @{ artifact = $gitStatusPath }
|
||
}
|
||
|
||
if (Test-Path -LiteralPath $LibraryRoot -PathType Container) {
|
||
Add-Check -Key "library_root" -Label "阶段 13 资料包根目录" -Status "pass" -Detail "资料包根目录存在" -Meta @{ root = $LibraryRoot }
|
||
} else {
|
||
Add-Check -Key "library_root" -Label "阶段 13 资料包根目录" -Status "fail" -Detail "资料包根目录缺失" -Meta @{ root = $LibraryRoot }
|
||
}
|
||
|
||
$inventoryRoot = Join-Path $LibraryRoot "inventories"
|
||
$reviewRegisterPath = Join-Path $inventoryRoot "expert-review-register.csv"
|
||
$legalSources = Import-CsvRows -Path (Join-Path $inventoryRoot "legal-source-inventory.csv")
|
||
$knowledgeCards = Import-CsvRows -Path (Join-Path $inventoryRoot "legal-knowledge-card-inventory.csv")
|
||
$reviewRules = Import-CsvRows -Path (Join-Path $inventoryRoot "review-risk-rule-inventory.csv")
|
||
$clauses = Import-CsvRows -Path (Join-Path $inventoryRoot "clause-library-inventory.csv")
|
||
$templates = Import-CsvRows -Path (Join-Path $inventoryRoot "contract-template-inventory.csv")
|
||
$reviewSamples = Import-CsvRows -Path (Join-Path $inventoryRoot "review-golden-sample-inventory.csv")
|
||
$generationSamples = Import-CsvRows -Path (Join-Path $inventoryRoot "generation-golden-sample-inventory.csv")
|
||
$kbQaSamples = Import-CsvRows -Path (Join-Path $inventoryRoot "kb-qa-golden-sample-inventory.csv")
|
||
$reviewRows = Import-CsvRows -Path $reviewRegisterPath
|
||
|
||
if ($reviewRows.Count -ge $MinReviewRegistrations) {
|
||
Add-Check -Key "review_register_minimum" -Label "复核登记数量下限" -Status "pass" -Detail "复核登记数量=$($reviewRows.Count),最低要求=$MinReviewRegistrations"
|
||
} else {
|
||
Add-Check -Key "review_register_minimum" -Label "复核登记数量下限" -Status "fail" -Detail "复核登记数量=$($reviewRows.Count),最低要求=$MinReviewRegistrations"
|
||
}
|
||
|
||
$assetMap = @{}
|
||
foreach ($row in $legalSources) {
|
||
$assetMap["legal_source:$($row.source_id)"] = New-AssetMeta -AssetType "legal_source" -AssetId $row.source_id -AssetName $row.title -Status $row.status -ProductionReady "" -LegalReviewer $row.legal_reviewer -BusinessReviewer "" -SourceIds $row.source_id
|
||
}
|
||
foreach ($row in $knowledgeCards) {
|
||
$assetMap["knowledge_card:$($row.knowledge_card_id)"] = New-AssetMeta -AssetType "knowledge_card" -AssetId $row.knowledge_card_id -AssetName $row.title -Status $row.status -ProductionReady $row.production_ready -LegalReviewer $row.legal_reviewer -BusinessReviewer "" -SourceIds $row.source_ids
|
||
}
|
||
foreach ($row in $reviewRules) {
|
||
$assetMap["review_rule:$($row.rule_id)"] = New-AssetMeta -AssetType "review_rule" -AssetId $row.rule_id -AssetName $row.risk_name -Status $row.status -ProductionReady $row.production_ready -LegalReviewer $row.legal_reviewer -BusinessReviewer "" -Severity $row.severity -SourceIds $row.source_ids
|
||
}
|
||
foreach ($row in $clauses) {
|
||
$assetMap["clause:$($row.clause_id)"] = New-AssetMeta -AssetType "clause" -AssetId $row.clause_id -AssetName $row.clause_name -Status $row.status -ProductionReady $row.production_ready -LegalReviewer $row.legal_reviewer -BusinessReviewer $row.business_reviewer -SourceIds $row.source_ids
|
||
}
|
||
foreach ($row in $templates) {
|
||
$assetMap["contract_template:$($row.template_id)"] = New-AssetMeta -AssetType "contract_template" -AssetId $row.template_id -AssetName $row.template_name -Status $row.status -ProductionReady $row.production_ready -LegalReviewer $row.legal_reviewer -BusinessReviewer $row.business_reviewer -SourceIds ""
|
||
}
|
||
foreach ($row in $reviewSamples) {
|
||
$assetMap["review_golden_sample:$($row.sample_id)"] = New-AssetMeta -AssetType "review_golden_sample" -AssetId $row.sample_id -AssetName $row.contract_type -Status $row.status -ProductionReady $row.production_ready -LegalReviewer $row.legal_reviewer -BusinessReviewer "" -SourceIds $row.source_ids
|
||
}
|
||
foreach ($row in $generationSamples) {
|
||
$assetMap["generation_golden_sample:$($row.sample_id)"] = New-AssetMeta -AssetType "generation_golden_sample" -AssetId $row.sample_id -AssetName $row.contract_type -Status $row.status -ProductionReady $row.production_ready -LegalReviewer $row.legal_reviewer -BusinessReviewer $row.business_reviewer -SourceIds $row.expected_source_citations
|
||
}
|
||
foreach ($row in $kbQaSamples) {
|
||
$assetMap["kb_qa_golden_sample:$($row.sample_id)"] = New-AssetMeta -AssetType "kb_qa_golden_sample" -AssetId $row.sample_id -AssetName $row.question -Status $row.status -ProductionReady $row.production_ready -LegalReviewer $row.legal_reviewer -BusinessReviewer "" -SourceIds $row.expected_source_ids
|
||
}
|
||
|
||
$duplicateReviewIds = @($reviewRows | Where-Object { -not [string]::IsNullOrWhiteSpace($_.review_id) } | Group-Object review_id | Where-Object { $_.Count -gt 1 } | Select-Object -ExpandProperty Name)
|
||
if ($duplicateReviewIds.Count -eq 0) {
|
||
Add-Check -Key "review_id_unique" -Label "复核任务编号唯一性" -Status "pass" -Detail "未发现重复复核任务编号"
|
||
} else {
|
||
Add-Check -Key "review_id_unique" -Label "复核任务编号唯一性" -Status "fail" -Detail "重复编号:$($duplicateReviewIds -join ',')" -Meta @{ duplicates = $duplicateReviewIds }
|
||
}
|
||
|
||
$allowedReviewResults = @("PENDING", "CHANGES_REQUIRED", "APPROVED_FOR_REVIEW_IMPORT", "APPROVED_FOR_PRODUCTION", "REJECTED")
|
||
$allowedReviewStatus = @("PENDING", "IN_PROGRESS", "CHANGES_REQUIRED", "COMPLETED", "REJECTED")
|
||
$reviewRequiredMissing = @()
|
||
$reviewStatusInvalid = @()
|
||
$reviewUnknownAssets = @()
|
||
$reviewEvidenceMissing = @()
|
||
$approvedWithoutDate = @()
|
||
$approvedWithoutHuman = @()
|
||
$taskBoard = New-Object System.Collections.Generic.List[object]
|
||
|
||
foreach ($row in $reviewRows) {
|
||
$assetKey = "$($row.asset_type):$($row.asset_id)"
|
||
$asset = if ($assetMap.ContainsKey($assetKey)) { $assetMap[$assetKey] } else { $null }
|
||
if ($null -eq $asset) {
|
||
$reviewUnknownAssets += $assetKey
|
||
}
|
||
|
||
$missing = New-Object System.Collections.Generic.List[string]
|
||
foreach ($field in @("review_id", "asset_type", "asset_id", "reviewer", "reviewer_role", "review_result", "required_changes", "follow_up_owner", "evidence_path", "status")) {
|
||
if ([string]::IsNullOrWhiteSpace($row.$field)) {
|
||
$missing.Add($field) | Out-Null
|
||
}
|
||
}
|
||
if ($missing.Count -gt 0) {
|
||
$reviewRequiredMissing += "$($row.review_id):$($missing -join ';')"
|
||
}
|
||
if ($allowedReviewResults -notcontains $row.review_result -or $allowedReviewStatus -notcontains $row.status) {
|
||
$reviewStatusInvalid += $row.review_id
|
||
}
|
||
if (-not (Test-LibraryFilePath -RelativePath $row.evidence_path)) {
|
||
$reviewEvidenceMissing += $row.review_id
|
||
}
|
||
|
||
$isApproved = @("APPROVED_FOR_REVIEW_IMPORT", "APPROVED_FOR_PRODUCTION") -contains $row.review_result
|
||
$reviewerPending = Test-PendingReviewer -Value $row.reviewer
|
||
if ($isApproved -and [string]::IsNullOrWhiteSpace($row.review_date)) {
|
||
$approvedWithoutDate += $row.review_id
|
||
}
|
||
if ($isApproved -and $reviewerPending) {
|
||
$approvedWithoutHuman += $row.review_id
|
||
}
|
||
|
||
$lane = Get-ReviewLane -AssetType $row.asset_type -ReviewerRole $row.reviewer_role
|
||
$priority = Get-ReviewPriority -AssetType $row.asset_type -Asset $asset
|
||
$blockers = New-Object System.Collections.Generic.List[string]
|
||
if ($reviewerPending) { $blockers.Add("reviewer_not_assigned") | Out-Null }
|
||
if ($row.review_result -eq "PENDING" -or $row.status -eq "PENDING") { $blockers.Add("review_pending") | Out-Null }
|
||
if ($null -eq $asset) { $blockers.Add("asset_not_found") | Out-Null }
|
||
if (-not (Test-LibraryFilePath -RelativePath $row.evidence_path)) { $blockers.Add("evidence_missing") | Out-Null }
|
||
|
||
$taskBoard.Add([pscustomobject][ordered]@{
|
||
priority = $priority
|
||
review_lane = $lane
|
||
asset_type = $row.asset_type
|
||
asset_type_text = Get-AssetTypeText -AssetType $row.asset_type
|
||
asset_id = $row.asset_id
|
||
asset_name = if ($asset) { $asset.asset_name } else { "" }
|
||
asset_status = if ($asset) { $asset.status } else { "" }
|
||
production_ready = if ($asset) { $asset.production_ready } else { "" }
|
||
review_id = $row.review_id
|
||
reviewer = $row.reviewer
|
||
reviewer_role = $row.reviewer_role
|
||
review_result = $row.review_result
|
||
review_status = $row.status
|
||
follow_up_owner = $row.follow_up_owner
|
||
required_changes = $row.required_changes
|
||
evidence_path = $row.evidence_path
|
||
blocker_count = $blockers.Count
|
||
blockers = ($blockers -join ";")
|
||
}) | Out-Null
|
||
}
|
||
|
||
$pendingRows = @($taskBoard | Where-Object { $_.review_result -eq "PENDING" -or $_.review_status -eq "PENDING" })
|
||
$unassignedRows = @($taskBoard | Where-Object { Test-PendingReviewer -Value $_.reviewer })
|
||
$blockedRows = @($taskBoard | Where-Object { $_.blocker_count -gt 0 })
|
||
$completedRows = @($taskBoard | Where-Object { @("APPROVED_FOR_REVIEW_IMPORT", "APPROVED_FOR_PRODUCTION", "REJECTED") -contains $_.review_result -and $_.review_status -in @("COMPLETED", "REJECTED") })
|
||
|
||
if ($reviewRequiredMissing.Count -eq 0) {
|
||
Add-Check -Key "review_required_fields" -Label "复核任务必填字段" -Status "pass" -Detail "复核任务必填字段均已填写"
|
||
} else {
|
||
Add-Check -Key "review_required_fields" -Label "复核任务必填字段" -Status "fail" -Detail "以下复核任务缺少必填字段:$($reviewRequiredMissing -join ',')" -Meta @{ rows = $reviewRequiredMissing }
|
||
}
|
||
if ($reviewStatusInvalid.Count -eq 0) {
|
||
Add-Check -Key "review_status_values" -Label "复核结果和状态枚举" -Status "pass" -Detail "复核结果和状态均在允许范围内"
|
||
} else {
|
||
Add-Check -Key "review_status_values" -Label "复核结果和状态枚举" -Status "fail" -Detail "以下复核任务状态无效:$($reviewStatusInvalid -join ',')" -Meta @{ rows = $reviewStatusInvalid }
|
||
}
|
||
if ($reviewUnknownAssets.Count -eq 0) {
|
||
Add-Check -Key "review_asset_traceability" -Label "复核任务资产可追溯" -Status "pass" -Detail "复核任务均绑定已登记资产"
|
||
} else {
|
||
Add-Check -Key "review_asset_traceability" -Label "复核任务资产可追溯" -Status "fail" -Detail "以下复核任务绑定未知资产:$($reviewUnknownAssets -join ',')" -Meta @{ rows = $reviewUnknownAssets }
|
||
}
|
||
if ($reviewEvidenceMissing.Count -eq 0) {
|
||
Add-Check -Key "review_evidence_files" -Label "复核证据模板" -Status "pass" -Detail "复核证据模板均存在"
|
||
} else {
|
||
Add-Check -Key "review_evidence_files" -Label "复核证据模板" -Status "fail" -Detail "以下复核任务缺少证据模板:$($reviewEvidenceMissing -join ',')" -Meta @{ rows = $reviewEvidenceMissing }
|
||
}
|
||
if ($approvedWithoutDate.Count -eq 0 -and $approvedWithoutHuman.Count -eq 0) {
|
||
Add-Check -Key "approved_review_integrity" -Label "已批准复核完整性" -Status "pass" -Detail "未发现缺少日期或真实复核人的已批准任务"
|
||
} else {
|
||
Add-Check -Key "approved_review_integrity" -Label "已批准复核完整性" -Status "fail" -Detail "缺少日期:$($approvedWithoutDate -join ',');缺少真实复核人:$($approvedWithoutHuman -join ',')" -Meta @{ without_date = $approvedWithoutDate; without_human = $approvedWithoutHuman }
|
||
}
|
||
if ($unassignedRows.Count -eq 0) {
|
||
Add-Check -Key "review_assignment" -Label "复核人分配" -Status "pass" -Detail "所有复核任务均已分配真实复核人"
|
||
} else {
|
||
Add-Check -Key "review_assignment" -Label "复核人分配" -Status "blocked" -Detail "仍有 $($unassignedRows.Count) 个复核任务未分配真实复核人" -Meta @{ count = $unassignedRows.Count }
|
||
}
|
||
if ($pendingRows.Count -eq 0) {
|
||
Add-Check -Key "review_execution" -Label "复核执行完成度" -Status "pass" -Detail "所有复核任务均已完成或拒绝"
|
||
} else {
|
||
Add-Check -Key "review_execution" -Label "复核执行完成度" -Status "blocked" -Detail "仍有 $($pendingRows.Count) 个复核任务处于 PENDING"
|
||
}
|
||
|
||
$firstWaveAssetTypes = @("contract_template", "clause", "review_rule", "generation_golden_sample")
|
||
$firstWaveRows = @($taskBoard | Where-Object { $firstWaveAssetTypes -contains $_.asset_type })
|
||
$firstWavePending = @($firstWaveRows | Where-Object { $_.blocker_count -gt 0 })
|
||
if ($firstWaveRows.Count -gt 0 -and $firstWavePending.Count -eq 0) {
|
||
Add-Check -Key "first_wave_review" -Label "第一批模板相关复核" -Status "pass" -Detail "模板、条款、规则和生成样本复核均已完成"
|
||
} elseif ($firstWaveRows.Count -eq 0) {
|
||
Add-Check -Key "first_wave_review" -Label "第一批模板相关复核" -Status "fail" -Detail "未找到模板相关复核任务"
|
||
} else {
|
||
Add-Check -Key "first_wave_review" -Label "第一批模板相关复核" -Status "blocked" -Detail "第一批模板相关复核仍有 $($firstWavePending.Count) 个阻塞项"
|
||
}
|
||
|
||
$taskBoard |
|
||
Sort-Object priority, review_lane, asset_type, asset_id |
|
||
Export-Csv -LiteralPath $taskBoardCsv -NoTypeInformation -Encoding UTF8
|
||
|
||
$blockedRows |
|
||
Sort-Object priority, review_lane, asset_type, asset_id |
|
||
Export-Csv -LiteralPath $blockersCsv -NoTypeInformation -Encoding UTF8
|
||
|
||
$batchSummary = @(
|
||
$taskBoard |
|
||
Group-Object priority, review_lane, asset_type |
|
||
ForEach-Object {
|
||
$groupRows = @($_.Group)
|
||
[pscustomobject][ordered]@{
|
||
priority = [string]$groupRows[0].priority
|
||
review_lane = [string]$groupRows[0].review_lane
|
||
asset_type = [string]$groupRows[0].asset_type
|
||
asset_type_text = [string]$groupRows[0].asset_type_text
|
||
total = $groupRows.Count
|
||
blocked = @($groupRows | Where-Object { $_.blocker_count -gt 0 }).Count
|
||
pending = @($groupRows | Where-Object { $_.review_result -eq "PENDING" -or $_.review_status -eq "PENDING" }).Count
|
||
completed = @($groupRows | Where-Object { @("APPROVED_FOR_REVIEW_IMPORT", "APPROVED_FOR_PRODUCTION", "REJECTED") -contains $_.review_result }).Count
|
||
}
|
||
}
|
||
)
|
||
$batchSummary |
|
||
Sort-Object priority, review_lane, asset_type |
|
||
Export-Csv -LiteralPath $batchSummaryCsv -NoTypeInformation -Encoding UTF8
|
||
|
||
$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]@{
|
||
review_registrations = $reviewRows.Count
|
||
task_board_rows = $taskBoard.Count
|
||
pending_reviews = $pendingRows.Count
|
||
unassigned_reviews = $unassignedRows.Count
|
||
blocked_reviews = $blockedRows.Count
|
||
completed_reviews = $completedRows.Count
|
||
first_wave_reviews = $firstWaveRows.Count
|
||
first_wave_blocked = $firstWavePending.Count
|
||
legal_lane_reviews = @($taskBoard | Where-Object { $_.review_lane -eq "法务" }).Count
|
||
legal_business_lane_reviews = @($taskBoard | Where-Object { $_.review_lane -eq "法务+业务" }).Count
|
||
p0_reviews = @($taskBoard | Where-Object { $_.priority -eq "P0" }).Count
|
||
p1_reviews = @($taskBoard | Where-Object { $_.priority -eq "P1" }).Count
|
||
p2_reviews = @($taskBoard | Where-Object { $_.priority -eq "P2" }).Count
|
||
}
|
||
|
||
$summary = [ordered]@{
|
||
name = "phase13-review-execution-gate"
|
||
status = $overallStatus
|
||
generated_at = (Get-Date).ToUniversalTime().ToString("o")
|
||
branch = $branch
|
||
commit = $commit
|
||
remote = $remoteUrl
|
||
artifact_dir = $artifactDir
|
||
library_root = $LibraryRoot
|
||
checks = $checks.ToArray()
|
||
metrics = $metrics
|
||
artifacts = [ordered]@{
|
||
summary_json = $summaryJson
|
||
summary_md = $summaryMd
|
||
task_board = $taskBoardCsv
|
||
batch_summary = $batchSummaryCsv
|
||
blockers = $blockersCsv
|
||
git_status = $gitStatusPath
|
||
}
|
||
totals = [ordered]@{
|
||
total_checks = $checks.Count
|
||
failed_required = $failedRequired
|
||
blocked_required = $blockedRequired
|
||
warnings = $warnings
|
||
}
|
||
}
|
||
Save-Json -Path $summaryJson -Value $summary
|
||
|
||
$statusText = Convert-StatusText -Status $overallStatus
|
||
$md = New-Object System.Collections.Generic.List[string]
|
||
$md.Add("# 阶段 13 法务与业务复核执行门禁") | Out-Null
|
||
$md.Add("") | Out-Null
|
||
$md.Add("- 门禁状态:$statusText($overallStatus)") | Out-Null
|
||
$md.Add("- 生成时间:$($summary.generated_at)") | Out-Null
|
||
$md.Add("- 分支:$branch") | Out-Null
|
||
$md.Add("- 提交:$commit") | Out-Null
|
||
$md.Add("- 资料包:$LibraryRoot") | 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.review_registrations) |") | Out-Null
|
||
$md.Add("| 任务看板行数 | $($metrics.task_board_rows) |") | Out-Null
|
||
$md.Add("| 待执行复核 | $($metrics.pending_reviews) |") | Out-Null
|
||
$md.Add("| 未分配真实复核人 | $($metrics.unassigned_reviews) |") | Out-Null
|
||
$md.Add("| 阻塞复核任务 | $($metrics.blocked_reviews) |") | Out-Null
|
||
$md.Add("| 已完成复核 | $($metrics.completed_reviews) |") | Out-Null
|
||
$md.Add("| 第一批模板相关复核 | $($metrics.first_wave_reviews) |") | Out-Null
|
||
$md.Add("| 第一批模板相关阻塞 | $($metrics.first_wave_blocked) |") | Out-Null
|
||
$md.Add("| P0 复核任务 | $($metrics.p0_reviews) |") | Out-Null
|
||
$md.Add("| P1 复核任务 | $($metrics.p1_reviews) |") | Out-Null
|
||
$md.Add("| P2 复核任务 | $($metrics.p2_reviews) |") | 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 { "否" }
|
||
$checkStatus = Convert-CheckStatusText -Status $check.status
|
||
$detail = ([string]$check.detail).Replace("|", "/")
|
||
$md.Add("| $($check.label) | $requiredText | $checkStatus | $detail |") | Out-Null
|
||
}
|
||
$md.Add("") | Out-Null
|
||
$md.Add("## 当前结论") | Out-Null
|
||
$md.Add("") | Out-Null
|
||
if ($overallStatus -eq "BLOCKED") {
|
||
$md.Add("复核执行门禁已建立,但真实复核尚未完成。下一步需要分配真实法务和业务复核人,并把任务从 PENDING 推进到 APPROVED 或 REJECTED。") | Out-Null
|
||
} elseif ($overallStatus -eq "PASS") {
|
||
$md.Add("复核执行门禁已通过,可以进入 Review 或 Production 入库前的发布门禁。") | Out-Null
|
||
} else {
|
||
$md.Add("复核执行门禁未完全通过,请查看任务看板和阻塞清单。") | Out-Null
|
||
}
|
||
$md.Add("") | Out-Null
|
||
$md.Add("本地产物:") | Out-Null
|
||
$md.Add("") | Out-Null
|
||
$md.Add('- `phase13-review-execution-gate-summary.json`') | Out-Null
|
||
$md.Add('- `phase13-review-execution-gate-summary.md`') | Out-Null
|
||
$md.Add('- `review-task-board.csv`') | Out-Null
|
||
$md.Add('- `review-batch-summary.csv`') | Out-Null
|
||
$md.Add('- `review-execution-blockers.csv`') | Out-Null
|
||
$md | Set-Content -Encoding UTF8 -Path $summaryMd
|
||
|
||
Write-Host "PHASE13_REVIEW_EXECUTION_GATE_$overallStatus $artifactDir"
|
||
if ($overallStatus -eq "FAIL") {
|
||
throw "阶段 13 法务与业务复核执行门禁未通过。查看:$summaryMd"
|
||
}
|
||
if ($overallStatus -eq "BLOCKED" -and $FailOnBlocked) {
|
||
throw "阶段 13 法务与业务复核执行门禁被阻塞。查看:$summaryMd"
|
||
}
|