docs: add phase 13 review execution gate

This commit is contained in:
Codex
2026-05-01 23:29:22 +08:00
parent 46a287da37
commit 5fd90367f3
6 changed files with 697 additions and 3 deletions
@@ -13,6 +13,7 @@
7. 合同审核、合同生成和知识库问答黄金样本字段规范。
8. 法律专家和业务负责人复核登记规范。
9. 法务复核知识库导入脚本。
10. 法务与业务复核执行门禁和任务看板。
重要规则:
@@ -36,4 +37,5 @@
10. 阶段 13.8 生产试点发布门禁脚本已建立,默认 `DryRun` 模式只做发布前验证,并在资料未复核或模板库缺失时输出阻塞证据。
11. 阶段 13.8 第一批中文合同模板已建立,共 5 个,覆盖采购、通用服务、SaaS、技术开发和商业保密协议,全部保持 `LEGAL_REVIEW` 且 `production_ready=false`。
12. 阶段 13.8 专家复核登记已建立,共 82 条,覆盖全部待复核法源、知识卡、规则、条款、模板和样本。
13. 生产发布仍需等待法务复核和业务复核完成。
13. 阶段 13.9 法务与业务复核执行门禁已建立,可生成复核任务看板、批次统计和阻塞清单。
14. 生产发布仍需等待真实法务复核和业务复核完成;当前阻塞项为 82 条复核任务均未分配真实复核人且仍处于 `PENDING`。
@@ -395,7 +395,7 @@ project-docs/phase13-legal-knowledge-template-library/
4. 生成对应标准条款初稿,仍保持 `LEGAL_REVIEW`。
5. 用 `scripts/import-phase13-library.ps1 -Mode Validate` 和阶段 13 门禁持续校验资料包。
阶段 13 当前状态:`IN_PROGRESS`。
阶段 13 当前状态:`IN_PROGRESS / BLOCKED_ON_LEGAL_AND_BUSINESS_REVIEW`。
当前第一批执行结果:`13.0` 和 `13.1` 已完成工程化底座,资料包、清单字段结构、第一批 `20` 条官方法源采集任务和 `scripts/run-legal-knowledge-library-gate.ps1` 已落地。门禁状态为 `PASS_WITH_LEGAL_REVIEW_REQUIRED`,所有法源在法务复核前不得作为生产确定性依据。
@@ -406,3 +406,5 @@ project-docs/phase13-legal-knowledge-template-library/
当前第七批执行结果:`13.8` 生产试点发布门禁脚本 `scripts/run-phase13-production-pilot-release.ps1` 已落地。脚本默认 `DryRun` 模式不写系统 API,会串联入库闭环并输出发布候选资产表和阻塞资产表;真实 `Review` 或 `Production` 发布必须显式传入 `-AllowApiRelease`。当前发布状态为 `BLOCKED`,原因是无已复核可发布资产且合同模板库为空。
当前第八批执行结果:`合同模板库补齐 + 法务复核登记` 已落地。第一批中文合同模板新增 `5` 个,覆盖采购、通用服务、SaaS、技术开发和商业保密协议;合同生成黄金样本增至 `13` 个;专家复核登记增至 `82` 条并覆盖全部待复核资产。资料门禁、黄金样本评测和入库校验均可通过,状态仍为 `PASS_WITH_LEGAL_REVIEW_REQUIRED`,因为资料尚未完成法务和业务复核。
当前第九批执行结果:`法务与业务复核执行门禁` 已落地。新增 `scripts/run-phase13-review-execution-gate.ps1`,可基于 `expert-review-register.csv` 生成复核任务看板、批次统计、阻塞清单和门禁摘要;生产试点发布脚本已串联该门禁。当前门禁状态为 `BLOCKED`,因为 `82` 条复核任务均未分配真实复核人且仍处于 `PENDING`;第一批模板相关 `34` 条复核任务全部阻塞。下一步必须由真实法务和业务负责人完成复核后,才能把资产从 `LEGAL_REVIEW` 推进到 `APPROVED` 或 `PUBLISHED`。
@@ -0,0 +1,120 @@
# AI合同平台 阶段 13 法律知识库与合同模板库建设闭环第九批执行记录(2026-05-01)
## 一、阶段结论
本批次完成 `法务与业务复核执行门禁` 闭环,当前状态为 `BLOCKED_ON_LEGAL_AND_BUSINESS_REVIEW`。
第八批已经补齐合同模板库和专家复核登记,但登记本身不能代表真实复核完成。本批次将复核登记转化为可复跑的执行门禁、复核任务看板、批次统计和阻塞清单,并把该门禁接入生产试点发布流程。
生产试点发布仍然不能执行,原因已经明确收敛为真实外部复核未完成:`82` 条复核任务均未分配真实复核人且仍处于 `PENDING`。
## 二、本批次交付
新增或更新资料:
1. `scripts/run-phase13-review-execution-gate.ps1`。
2. `scripts/run-phase13-production-pilot-release.ps1`,新增复核执行门禁子流程。
3. `scripts/run-legal-knowledge-library-gate.ps1`,将复核执行门禁脚本纳入通用文件检查和敏感信息扫描。
4. `project-docs/phase13-legal-knowledge-template-library/README.md`,更新阶段 13 资料包当前状态。
5. `project-docs/阶段13-法律知识库与合同模板库建设闭环方案-2026-05-01.md`,追加第九批执行结果。
新增门禁产物:
1. `phase13-review-execution-gate-summary.json`。
2. `phase13-review-execution-gate-summary.md`。
3. `review-task-board.csv`。
4. `review-batch-summary.csv`。
5. `review-execution-blockers.csv`。
## 三、门禁能力
本批次门禁检查以下复核执行条件:
1. gstack 前置条件。
2. 内网 `origin` 远端仓库。
3. 阶段 13 资料包根目录。
4. 复核登记数量下限。
5. 复核任务编号唯一性。
6. 复核任务必填字段。
7. 复核结果和状态枚举。
8. 复核任务绑定资产可追溯。
9. 复核证据模板文件存在。
10. 已批准复核任务必须有真实复核人和复核日期。
11. 未分配真实复核人的任务阻塞发布。
12. 仍处于 `PENDING` 的任务阻塞发布。
13. 第一批模板相关复核未完成时阻塞发布。
## 四、自动化验证结果
复核执行门禁:
```text
PHASE13_REVIEW_EXECUTION_GATE_BLOCKED D:\ai-contract-platform\project-docs\qa-artifacts\phase13-review-execution-gate\2026-05-01T23-27-14-543
```
资料门禁:
```text
PHASE13_LEGAL_KNOWLEDGE_LIBRARY_GATE_PASS_WITH_LEGAL_REVIEW_REQUIRED D:\ai-contract-platform\project-docs\qa-artifacts\phase13-legal-knowledge-library-gate\2026-05-01T23-27-20-997
```
生产试点发布门禁:
```text
PHASE13_PRODUCTION_PILOT_RELEASE_BLOCKED D:\ai-contract-platform\project-docs\qa-artifacts\phase13-production-pilot-release\2026-05-01T23-27-29-862
```
生产试点发布门禁内部子流程:
```text
ingestion_closed_loop=PASS_WITH_LEGAL_REVIEW_REQUIRED D:\ai-contract-platform\project-docs\qa-artifacts\phase13-ingestion-closed-loop\2026-05-01T23-27-31-860
review_execution_gate=BLOCKED D:\ai-contract-platform\project-docs\qa-artifacts\phase13-review-execution-gate\2026-05-01T23-27-40-723
```
## 五、关键统计
| 指标 | 结果 |
|---|---:|
| 复核登记 | 82 |
| 复核任务看板行数 | 82 |
| 待执行复核 | 82 |
| 未分配真实复核人 | 82 |
| 阻塞复核任务 | 82 |
| 已完成复核 | 0 |
| 第一批模板相关复核 | 34 |
| 第一批模板相关阻塞 | 34 |
| 法务复核通道 | 56 |
| 法务+业务复核通道 | 26 |
| P0 复核任务 | 22 |
| P1 复核任务 | 42 |
| P2 复核任务 | 18 |
| 发布候选资产 | 82 |
| 可发布资产 | 0 |
| 发布阻塞资产 | 82 |
## 六、仍然阻塞发布的原因
生产试点发布门禁当前阻塞项:
1. 可发布法源数量为 0。
2. 可发布知识卡数量为 0。
3. 可发布审核规则数量为 0。
4. 可发布标准条款数量为 0。
5. 可发布合同模板数量为 0。
6. `82` 条复核任务未分配真实复核人。
7. `82` 条复核任务仍处于 `PENDING`。
8. 第一批模板相关 `34` 条复核任务全部阻塞。
本批次没有伪造人工复核结果,也没有将 `LEGAL_REVIEW` 资产改为 `APPROVED` 或 `PUBLISHED`。真实复核必须由法务或业务负责人执行。
## 七、下一步
下一批建议进入 `真实复核执行与首批资产批准`:
1. 先分配真实法务复核人和业务复核人。
2. 优先处理 P0 复核任务:5 个合同模板、5 个模板生成样本、8 个标准条款、4 条高风险审核规则。
3. 对通过复核的资产补充复核日期、复核人、复核意见和证据记录。
4. 仅在真实复核完成后,将资产状态推进到 `APPROVED`,满足试点条件时再设置 `production_ready=true`。
5. 重新运行复核执行门禁和生产试点发布门禁,直到首批资产进入可发布状态。
阶段 13 第九批当前状态:`BLOCKED_ON_LEGAL_AND_BUSINESS_REVIEW`。
+6 -1
View File
@@ -281,6 +281,10 @@ function Test-SecretLeak {
if (Test-Path -LiteralPath $pilotReleaseScriptPath -PathType Leaf) {
$files += Get-Item -LiteralPath $pilotReleaseScriptPath
}
$reviewExecutionScriptPath = Join-Path $repoRoot "scripts\run-phase13-review-execution-gate.ps1"
if (Test-Path -LiteralPath $reviewExecutionScriptPath -PathType Leaf) {
$files += Get-Item -LiteralPath $reviewExecutionScriptPath
}
foreach ($file in $files) {
$match = Select-String -LiteralPath $file.FullName -Pattern $adminPasswordLiteral -List -ErrorAction SilentlyContinue
if ($match) {
@@ -351,7 +355,8 @@ $commonDocFiles = @(
"scripts\import-phase13-library.ps1",
"scripts\run-phase13-golden-eval.ps1",
"scripts\run-phase13-ingestion-closed-loop.ps1",
"scripts\run-phase13-production-pilot-release.ps1"
"scripts\run-phase13-production-pilot-release.ps1",
"scripts\run-phase13-review-execution-gate.ps1"
)
foreach ($docPath in $commonDocFiles) {
$absoluteDocPath = Join-Path $repoRoot $docPath
@@ -402,6 +402,14 @@ $ingestionRun = Invoke-StageScript `
-ExpectedPrefix "PHASE13_INGESTION_CLOSED_LOOP_" `
-SummaryFileName "phase13-ingestion-closed-loop-summary.json"
$reviewExecutionRun = Invoke-StageScript `
-Key "review_execution_gate" `
-Label "法务与业务复核执行门禁" `
-ScriptRelativePath "scripts\run-phase13-review-execution-gate.ps1" `
-Arguments @("-LibraryRoot", $LibraryRoot) `
-ExpectedPrefix "PHASE13_REVIEW_EXECUTION_GATE_" `
-SummaryFileName "phase13-review-execution-gate-summary.json"
if ($ReleaseMode -eq "DryRun") {
Add-Check -Key "api_release_guard" -Label "真实 API 发布保护" -Status "skip" -Required $false -Detail "DryRun 模式不写入系统 API"
} elseif ($AllowApiRelease) {
@@ -0,0 +1,557 @@
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"
}