diff --git a/project-docs/phase13-legal-knowledge-template-library/README.md b/project-docs/phase13-legal-knowledge-template-library/README.md index 2a26c7ac..650efa11 100644 --- a/project-docs/phase13-legal-knowledge-template-library/README.md +++ b/project-docs/phase13-legal-knowledge-template-library/README.md @@ -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`。 diff --git a/project-docs/阶段13-法律知识库与合同模板库建设闭环方案-2026-05-01.md b/project-docs/阶段13-法律知识库与合同模板库建设闭环方案-2026-05-01.md index 9c70deba..fb76a11b 100644 --- a/project-docs/阶段13-法律知识库与合同模板库建设闭环方案-2026-05-01.md +++ b/project-docs/阶段13-法律知识库与合同模板库建设闭环方案-2026-05-01.md @@ -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`。 diff --git a/project-docs/阶段13-法律知识库与合同模板库建设闭环第九批执行记录-2026-05-01.md b/project-docs/阶段13-法律知识库与合同模板库建设闭环第九批执行记录-2026-05-01.md new file mode 100644 index 00000000..c38c415d --- /dev/null +++ b/project-docs/阶段13-法律知识库与合同模板库建设闭环第九批执行记录-2026-05-01.md @@ -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`。 diff --git a/scripts/run-legal-knowledge-library-gate.ps1 b/scripts/run-legal-knowledge-library-gate.ps1 index a77b4f68..97ea359e 100644 --- a/scripts/run-legal-knowledge-library-gate.ps1 +++ b/scripts/run-legal-knowledge-library-gate.ps1 @@ -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 diff --git a/scripts/run-phase13-production-pilot-release.ps1 b/scripts/run-phase13-production-pilot-release.ps1 index 98bd097b..4e1e6c16 100644 --- a/scripts/run-phase13-production-pilot-release.ps1 +++ b/scripts/run-phase13-production-pilot-release.ps1 @@ -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) { diff --git a/scripts/run-phase13-review-execution-gate.ps1 b/scripts/run-phase13-review-execution-gate.ps1 new file mode 100644 index 00000000..43e03ba6 --- /dev/null +++ b/scripts/run-phase13-review-execution-gate.ps1 @@ -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" +}