Files
ai-contract-platform/scripts/run-phase13-review-execution-gate.ps1
T
2026-05-01 23:29:22 +08:00

558 lines
25 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]$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"
}