diff --git a/project-docs/phase13-legal-knowledge-template-library/README.md b/project-docs/phase13-legal-knowledge-template-library/README.md index 650efa11..6c1b5676 100644 --- a/project-docs/phase13-legal-knowledge-template-library/README.md +++ b/project-docs/phase13-legal-knowledge-template-library/README.md @@ -14,6 +14,7 @@ 8. 法律专家和业务负责人复核登记规范。 9. 法务复核知识库导入脚本。 10. 法务与业务复核执行门禁和任务看板。 +11. 首批 P0 复核包、复核分配模板、复核结果回填模板和 DryRun 回填门禁。 重要规则: @@ -38,4 +39,6 @@ 11. 阶段 13.8 第一批中文合同模板已建立,共 5 个,覆盖采购、通用服务、SaaS、技术开发和商业保密协议,全部保持 `LEGAL_REVIEW` 且 `production_ready=false`。 12. 阶段 13.8 专家复核登记已建立,共 82 条,覆盖全部待复核法源、知识卡、规则、条款、模板和样本。 13. 阶段 13.9 法务与业务复核执行门禁已建立,可生成复核任务看板、批次统计和阻塞清单。 -14. 生产发布仍需等待真实法务复核和业务复核完成;当前阻塞项为 82 条复核任务均未分配真实复核人且仍处于 `PENDING`。 +14. 阶段 13.10 首批 P0 复核包就绪门禁已建立,可生成 22 个 P0 复核任务的分配模板、结果回填模板和单项复核表。 +15. 阶段 13.10 复核结果回填门禁已建立,默认 DryRun 不写入登记表,真实回填必须显式传入 `-AllowWrite` 且通过完整性校验。 +16. 生产发布仍需等待真实法务复核和业务复核完成;当前阻塞项为 82 条复核任务均未分配真实复核人且仍处于 `PENDING`。 diff --git a/project-docs/阶段13-法律知识库与合同模板库建设闭环方案-2026-05-01.md b/project-docs/阶段13-法律知识库与合同模板库建设闭环方案-2026-05-01.md index fb76a11b..feb9f12a 100644 --- a/project-docs/阶段13-法律知识库与合同模板库建设闭环方案-2026-05-01.md +++ b/project-docs/阶段13-法律知识库与合同模板库建设闭环方案-2026-05-01.md @@ -408,3 +408,5 @@ project-docs/phase13-legal-knowledge-template-library/ 当前第八批执行结果:`合同模板库补齐 + 法务复核登记` 已落地。第一批中文合同模板新增 `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`。 + +当前第十批执行结果:`首批 P0 复核包就绪门禁` 已落地。新增 `scripts/run-phase13-first-wave-review-readiness.ps1`,默认选择 P0 任务并生成复核分配模板、复核结果回填模板、首批复核清单和逐项复核表。当前首批范围为 `22` 个任务:`5` 个合同模板、`5` 个模板生成样本、`8` 个标准条款和 `4` 条高风险审核规则。新增 `scripts/apply-phase13-review-results.ps1`,用于对真实复核结果进行 DryRun 校验和受保护回填;默认不写入登记表,只有显式传入 `-AllowWrite` 且没有阻塞项时才会更新复核登记。当前两个门禁均为 `BLOCKED`,因为尚未提供真实法务复核人、真实业务复核人、复核截止日期和真实复核结果;这保证了资料包可以交付给人执行,但不能把未复核资料伪装成已批准资产。 diff --git a/project-docs/阶段13-法律知识库与合同模板库建设闭环第十批执行记录-2026-05-01.md b/project-docs/阶段13-法律知识库与合同模板库建设闭环第十批执行记录-2026-05-01.md new file mode 100644 index 00000000..2c2f8309 --- /dev/null +++ b/project-docs/阶段13-法律知识库与合同模板库建设闭环第十批执行记录-2026-05-01.md @@ -0,0 +1,132 @@ +# AI合同平台 阶段 13 法律知识库与合同模板库建设闭环第十批执行记录(2026-05-01) + +## 一、阶段结论 + +本批次完成 `真实复核执行与首批资产批准准备` 闭环,当前状态为 `BLOCKED_ON_REAL_REVIEW_INPUT`。 + +第九批已经能证明 82 条复核任务未完成并阻断生产发布。本批次继续推进到可交付给人的执行层:生成首批 P0 复核包、复核分配模板、复核结果回填模板、逐项复核表,并新增复核结果 DryRun 回填门禁。 + +本批次没有伪造任何法务或业务复核结论,也没有把任何资产改成 `APPROVED` 或 `production_ready=true`。当前阻塞原因是缺少真实法务复核人、真实业务复核人、复核截止日期和真实复核结果。 + +## 二、本批次交付 + +新增或更新资料: + +1. `scripts/run-phase13-first-wave-review-readiness.ps1`。 +2. `scripts/apply-phase13-review-results.ps1`。 +3. `scripts/run-legal-knowledge-library-gate.ps1`,将首批复核包门禁和复核结果回填门禁纳入脚本存在性检查和敏感信息扫描。 +4. `project-docs/phase13-legal-knowledge-template-library/README.md`。 +5. `project-docs/阶段13-法律知识库与合同模板库建设闭环方案-2026-05-01.md`。 + +首批复核包产物: + +1. `phase13-first-wave-review-readiness-summary.json`。 +2. `phase13-first-wave-review-readiness-summary.md`。 +3. `review-assignment-template.csv`。 +4. `review-result-import-template.csv`。 +5. `selected-review-tasks.csv`。 +6. `first-wave-review-checklist.md`。 +7. `review-forms/`,共 22 份逐项复核表。 + +复核结果回填产物: + +1. `phase13-review-result-apply-summary.json`。 +2. `phase13-review-result-apply-summary.md`。 +3. `review-result-apply-plan.csv`。 +4. `review-result-apply-blockers.csv`。 + +## 三、首批复核范围 + +本批次默认选择 P0 任务作为首批复核范围: + +| 类型 | 数量 | +|---|---:| +| P0 复核任务 | 22 | +| 合同模板 | 5 | +| 模板生成样本 | 5 | +| 标准条款 | 8 | +| 高风险审核规则 | 4 | +| 法务通道任务 | 4 | +| 法务+业务通道任务 | 18 | + +这批任务覆盖合同生成上线最危险、最关键的资产:模板正文、模板生成样本、标准条款和高风险审核规则。 + +## 四、自动化验证结果 + +首批复核就绪门禁: + +```text +PHASE13_FIRST_WAVE_REVIEW_READINESS_BLOCKED D:\ai-contract-platform\project-docs\qa-artifacts\phase13-first-wave-review-readiness\2026-05-01T23-39-45-907 +``` + +复核结果回填门禁: + +```text +PHASE13_REVIEW_RESULT_APPLY_BLOCKED D:\ai-contract-platform\project-docs\qa-artifacts\phase13-review-result-apply\2026-05-01T23-39-54-328 +``` + +复核执行门禁: + +```text +PHASE13_REVIEW_EXECUTION_GATE_BLOCKED D:\ai-contract-platform\project-docs\qa-artifacts\phase13-review-execution-gate\2026-05-01T23-40-01-637 +``` + +资料门禁: + +```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-40-12-848 +``` + +生产试点发布门禁: + +```text +PHASE13_PRODUCTION_PILOT_RELEASE_BLOCKED D:\ai-contract-platform\project-docs\qa-artifacts\phase13-production-pilot-release\2026-05-01T23-40-21-970 +``` + +生产试点发布门禁内部子流程: + +```text +ingestion_closed_loop=PASS_WITH_LEGAL_REVIEW_REQUIRED D:\ai-contract-platform\project-docs\qa-artifacts\phase13-ingestion-closed-loop\2026-05-01T23-40-24-242 +review_execution_gate=BLOCKED D:\ai-contract-platform\project-docs\qa-artifacts\phase13-review-execution-gate\2026-05-01T23-40-32-905 +``` + +## 五、关键统计 + +| 指标 | 结果 | +|---|---:| +| 首批复核任务 | 22 | +| 缺少真实法务复核人任务 | 22 | +| 缺少真实业务复核人任务 | 18 | +| 是否缺少复核截止日期 | 是 | +| 复核结果回填行 | 22 | +| 回填阻塞项 | 22 | +| 仍待复核 | 22 | +| 可进入试点复核库 | 0 | +| 可进入生产发布 | 0 | +| 发布候选资产 | 82 | +| 可发布资产 | 0 | +| 发布阻塞资产 | 82 | + +## 六、安全边界 + +本批次新增的回填脚本默认 `DryRun`: + +1. 不写入 `expert-review-register.csv`。 +2. 不修改任何资产清单中的 `status`。 +3. 不修改任何资产清单中的 `production_ready`。 +4. 发现 `PENDING`、缺少真实复核人、缺少复核日期、缺少证据文件或未获生产批准却设置 `production_ready=true` 时,回填门禁保持 `BLOCKED`。 +5. 只有显式传入 `-AllowWrite` 且无阻塞项时,才允许回填复核登记。 + +## 七、下一步 + +下一步需要真实外部输入: + +1. 指定真实法务复核人。 +2. 指定真实业务复核人。 +3. 指定首批 P0 复核截止日期。 +4. 由复核人按 `review-forms/` 完成 22 项复核。 +5. 在 `review-result-import-template.csv` 中回填复核日期、复核结论、修改要求和证据路径。 +6. 使用 `scripts/apply-phase13-review-results.ps1` 先 DryRun 校验,再在确认无阻塞后显式使用 `-AllowWrite` 回填登记。 +7. 重新运行复核执行门禁和生产试点发布门禁。 + +阶段 13 第十批当前状态:`BLOCKED_ON_REAL_REVIEW_INPUT`。 diff --git a/scripts/apply-phase13-review-results.ps1 b/scripts/apply-phase13-review-results.ps1 new file mode 100644 index 00000000..9f650004 --- /dev/null +++ b/scripts/apply-phase13-review-results.ps1 @@ -0,0 +1,449 @@ +param( + [string]$ReviewResultsPath = "", + [string]$LibraryRoot = "", + [string]$ArtifactRoot = "", + [switch]$AllowWrite, + [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-result-apply" +} +if (-not [System.IO.Path]::IsPathRooted($ArtifactRoot)) { + $ArtifactRoot = Join-Path $repoRoot $ArtifactRoot +} + +if ([string]::IsNullOrWhiteSpace($ReviewResultsPath)) { + $packetRoot = Join-Path $repoRoot "project-docs\qa-artifacts\phase13-first-wave-review-readiness" + $latestPacket = Get-ChildItem -LiteralPath $packetRoot -Directory -ErrorAction SilentlyContinue | + Sort-Object Name -Descending | + Select-Object -First 1 + if ($latestPacket) { + $ReviewResultsPath = Join-Path $latestPacket.FullName "review-result-import-template.csv" + } +} +if (-not [string]::IsNullOrWhiteSpace($ReviewResultsPath) -and -not [System.IO.Path]::IsPathRooted($ReviewResultsPath)) { + $ReviewResultsPath = Join-Path $repoRoot $ReviewResultsPath +} + +$artifactDir = Join-Path $ArtifactRoot $timestamp +New-Item -ItemType Directory -Force -Path $artifactDir | Out-Null + +$summaryJson = Join-Path $artifactDir "phase13-review-result-apply-summary.json" +$summaryMd = Join-Path $artifactDir "phase13-review-result-apply-summary.md" +$applyPlanCsv = Join-Path $artifactDir "review-result-apply-plan.csv" +$blockersCsv = Join-Path $artifactDir "review-result-apply-blockers.csv" +$gitStatusPath = Join-Path $artifactDir "git-status.txt" + +$checks = New-Object System.Collections.Generic.List[object] +$applyPlan = New-Object System.Collections.Generic.List[object] +$blockers = 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 Get-FieldValue { + param([object]$Row, [string]$Name) + if ($null -eq $Row) { + return "" + } + $property = $Row.PSObject.Properties[$Name] + if ($null -eq $property -or $null -eq $property.Value) { + return "" + } + return [string]$property.Value +} + +function Test-PendingReviewer { + param([string]$Value) + if ([string]::IsNullOrWhiteSpace($Value)) { + return $true + } + return @("pending", "待法务复核", "待业务复核", "待法务和业务复核", "待复核") -contains $Value.Trim() +} + +function Test-Truthy { + param([string]$Value) + if ([string]::IsNullOrWhiteSpace($Value)) { + return $false + } + return @("true", "yes", "1", "required") -contains $Value.ToLowerInvariant() +} + +function Test-LibraryFilePath { + param([string]$RelativePath) + if ([string]::IsNullOrWhiteSpace($RelativePath)) { + return $false + } + if ([System.IO.Path]::IsPathRooted($RelativePath)) { + return Test-Path -LiteralPath $RelativePath -PathType Leaf + } + return Test-Path -LiteralPath (Join-Path $LibraryRoot ($RelativePath -replace "/", "\")) -PathType Leaf +} + +function Convert-OverallStatusText { + param([string]$Status) + switch ($Status) { + "PASS" { return "通过" } + "WARN" { return "通过但有警告" } + "FAIL" { return "未通过" } + "BLOCKED" { return "阻塞" } + default { return $Status } + } +} + +function Convert-CheckStatusText { + param([string]$Status) + switch ($Status) { + "pass" { return "通过" } + "fail" { return "失败" } + "warn" { return "警告" } + "skip" { return "跳过" } + "blocked" { return "阻塞" } + default { return $Status } + } +} + +function Add-Blocker { + param([string]$ReviewId, [string]$Reason, [string]$Detail) + $blockers.Add([pscustomobject][ordered]@{ + review_id = $ReviewId + reason = $Reason + detail = $Detail + }) | Out-Null +} + +function Export-Register { + param([object[]]$Rows, [string]$Path) + $Rows | Export-Csv -LiteralPath $Path -NoTypeInformation -Encoding UTF8 +} + +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 } +} + +if (-not [string]::IsNullOrWhiteSpace($ReviewResultsPath) -and (Test-Path -LiteralPath $ReviewResultsPath -PathType Leaf)) { + Add-Check -Key "review_results_file" -Label "复核结果文件" -Status "pass" -Detail "复核结果文件存在" -Meta @{ path = $ReviewResultsPath } +} else { + Add-Check -Key "review_results_file" -Label "复核结果文件" -Status "fail" -Detail "复核结果文件不存在" -Meta @{ path = $ReviewResultsPath } +} + +$inventoryRoot = Join-Path $LibraryRoot "inventories" +$reviewRegisterPath = Join-Path $inventoryRoot "expert-review-register.csv" +$reviewRows = Import-CsvRows -Path $reviewRegisterPath +$resultRows = Import-CsvRows -Path $ReviewResultsPath + +$registerMap = @{} +foreach ($row in $reviewRows) { + $registerMap[$row.review_id] = $row +} + +$allowedResults = @("PENDING", "CHANGES_REQUIRED", "APPROVED_FOR_REVIEW_IMPORT", "APPROVED_FOR_PRODUCTION", "REJECTED") +$allowedStatuses = @("PENDING", "IN_PROGRESS", "CHANGES_REQUIRED", "COMPLETED", "REJECTED") + +$pendingCount = 0 +$approvedForReviewCount = 0 +$approvedForProductionCount = 0 +$changesRequiredCount = 0 +$rejectedCount = 0 + +foreach ($row in $resultRows) { + $reviewId = Get-FieldValue -Row $row -Name "review_id" + $assetType = Get-FieldValue -Row $row -Name "asset_type" + $assetId = Get-FieldValue -Row $row -Name "asset_id" + $reviewer = Get-FieldValue -Row $row -Name "reviewer" + $reviewDate = Get-FieldValue -Row $row -Name "review_date" + $reviewResult = Get-FieldValue -Row $row -Name "review_result" + $reviewStatus = Get-FieldValue -Row $row -Name "status" + $evidencePath = Get-FieldValue -Row $row -Name "evidence_path" + $assetStatusAfterReview = Get-FieldValue -Row $row -Name "asset_status_after_review" + $productionReadyAfterReview = Get-FieldValue -Row $row -Name "production_ready_after_review" + + if ([string]::IsNullOrWhiteSpace($reviewId)) { + Add-Blocker -ReviewId "" -Reason "missing_review_id" -Detail "复核结果行缺少 review_id" + continue + } + if (-not $registerMap.ContainsKey($reviewId)) { + Add-Blocker -ReviewId $reviewId -Reason "unknown_review_id" -Detail "复核任务不在 expert-review-register.csv 中" + continue + } + + $registerRow = $registerMap[$reviewId] + if ($assetType -ne $registerRow.asset_type -or $assetId -ne $registerRow.asset_id) { + Add-Blocker -ReviewId $reviewId -Reason "asset_mismatch" -Detail "复核结果资产与登记表不一致" + } + if ($allowedResults -notcontains $reviewResult) { + Add-Blocker -ReviewId $reviewId -Reason "invalid_review_result" -Detail "复核结果枚举非法:$reviewResult" + } + if ($allowedStatuses -notcontains $reviewStatus) { + Add-Blocker -ReviewId $reviewId -Reason "invalid_status" -Detail "复核状态枚举非法:$reviewStatus" + } + + $isApproved = @("APPROVED_FOR_REVIEW_IMPORT", "APPROVED_FOR_PRODUCTION") -contains $reviewResult + if ($reviewResult -eq "PENDING" -or $reviewStatus -eq "PENDING") { + $pendingCount += 1 + Add-Blocker -ReviewId $reviewId -Reason "pending_review" -Detail "复核结果仍为 PENDING" + } elseif ($reviewResult -eq "CHANGES_REQUIRED") { + $changesRequiredCount += 1 + } elseif ($reviewResult -eq "REJECTED") { + $rejectedCount += 1 + } elseif ($reviewResult -eq "APPROVED_FOR_REVIEW_IMPORT") { + $approvedForReviewCount += 1 + } elseif ($reviewResult -eq "APPROVED_FOR_PRODUCTION") { + $approvedForProductionCount += 1 + } + + if ($isApproved -and (Test-PendingReviewer -Value $reviewer)) { + Add-Blocker -ReviewId $reviewId -Reason "approved_without_human_reviewer" -Detail "已批准结果缺少真实复核人" + } + if ($isApproved -and [string]::IsNullOrWhiteSpace($reviewDate)) { + Add-Blocker -ReviewId $reviewId -Reason "approved_without_review_date" -Detail "已批准结果缺少复核日期" + } + if ($isApproved -and -not (Test-LibraryFilePath -RelativePath $evidencePath)) { + Add-Blocker -ReviewId $reviewId -Reason "approved_without_evidence" -Detail "已批准结果缺少可追溯证据文件" + } + if ((Test-Truthy -Value $productionReadyAfterReview) -and $reviewResult -ne "APPROVED_FOR_PRODUCTION") { + Add-Blocker -ReviewId $reviewId -Reason "production_ready_without_production_approval" -Detail "未获得生产批准却尝试设置 production_ready=true" + } + if (@("APPROVED", "PUBLISHED", "LEGAL_REVIEW", "DRAFT", "DEPRECATED", "") -notcontains $assetStatusAfterReview) { + Add-Blocker -ReviewId $reviewId -Reason "invalid_asset_status_after_review" -Detail "资产复核后状态非法:$assetStatusAfterReview" + } + + $applyAction = "no_write_dry_run" + if ($AllowWrite) { + $applyAction = if ($reviewResult -eq "PENDING") { "skip_pending" } else { "update_review_register" } + } + + $applyPlan.Add([pscustomobject][ordered]@{ + review_id = $reviewId + asset_type = $assetType + asset_id = $assetId + reviewer = $reviewer + review_date = $reviewDate + review_result = $reviewResult + status = $reviewStatus + asset_status_after_review = $assetStatusAfterReview + production_ready_after_review = $productionReadyAfterReview + action = $applyAction + }) | Out-Null +} + +if ($resultRows.Count -gt 0) { + Add-Check -Key "review_result_rows" -Label "复核结果行数" -Status "pass" -Detail "复核结果行数=$($resultRows.Count)" +} else { + Add-Check -Key "review_result_rows" -Label "复核结果行数" -Status "fail" -Detail "复核结果为空" +} + +if ($blockers.Count -eq 0) { + Add-Check -Key "review_result_integrity" -Label "复核结果完整性" -Status "pass" -Detail "复核结果完整性通过" +} else { + Add-Check -Key "review_result_integrity" -Label "复核结果完整性" -Status "blocked" -Detail "发现 $($blockers.Count) 个阻塞项" -Meta @{ blockers = $blockers } +} + +if ($AllowWrite) { + if ($blockers.Count -eq 0) { + foreach ($row in $resultRows) { + $reviewId = Get-FieldValue -Row $row -Name "review_id" + $registerRow = $registerMap[$reviewId] + $registerRow.reviewer = Get-FieldValue -Row $row -Name "reviewer" + $registerRow.reviewer_role = Get-FieldValue -Row $row -Name "reviewer_role" + $registerRow.review_date = Get-FieldValue -Row $row -Name "review_date" + $registerRow.review_result = Get-FieldValue -Row $row -Name "review_result" + $registerRow.required_changes = Get-FieldValue -Row $row -Name "required_changes" + $registerRow.follow_up_owner = Get-FieldValue -Row $row -Name "follow_up_owner" + $registerRow.evidence_path = Get-FieldValue -Row $row -Name "evidence_path" + $registerRow.status = Get-FieldValue -Row $row -Name "status" + $registerRow.notes = Get-FieldValue -Row $row -Name "notes" + } + Export-Register -Rows $reviewRows -Path $reviewRegisterPath + Add-Check -Key "write_guard" -Label "复核结果写入保护" -Status "pass" -Detail "已显式允许写入,复核登记表已更新" -Meta @{ register = $reviewRegisterPath } + } else { + Add-Check -Key "write_guard" -Label "复核结果写入保护" -Status "blocked" -Detail "存在阻塞项,拒绝写入复核登记表" + } +} else { + Add-Check -Key "write_guard" -Label "复核结果写入保护" -Status "skip" -Required $false -Detail "默认 DryRun,不写入复核登记表;真实回填需显式传入 -AllowWrite" +} + +$applyPlan | Export-Csv -LiteralPath $applyPlanCsv -NoTypeInformation -Encoding UTF8 +$blockers | Export-Csv -LiteralPath $blockersCsv -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]@{ + result_rows = $resultRows.Count + apply_plan_rows = $applyPlan.Count + blockers = $blockers.Count + pending_reviews = $pendingCount + approved_for_review_import = $approvedForReviewCount + approved_for_production = $approvedForProductionCount + changes_required = $changesRequiredCount + rejected = $rejectedCount + allow_write = [bool]$AllowWrite +} + +$summary = [ordered]@{ + name = "phase13-review-result-apply" + status = $overallStatus + generated_at = (Get-Date).ToUniversalTime().ToString("o") + branch = $branch + commit = $commit + remote = $remoteUrl + library_root = $LibraryRoot + review_results_path = $ReviewResultsPath + checks = $checks.ToArray() + metrics = $metrics + artifacts = [ordered]@{ + summary_json = $summaryJson + summary_md = $summaryMd + apply_plan = $applyPlanCsv + 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 + +$md = New-Object System.Collections.Generic.List[string] +$md.Add("# 阶段 13 复核结果回填门禁") | Out-Null +$md.Add("") | Out-Null +$md.Add("- 门禁状态:$(Convert-OverallStatusText -Status $overallStatus)($overallStatus)") | Out-Null +$md.Add("- 生成时间:$($summary.generated_at)") | Out-Null +$md.Add("- 分支:$branch") | Out-Null +$md.Add("- 提交:$commit") | Out-Null +$md.Add("- 复核结果文件:$ReviewResultsPath") | Out-Null +$md.Add("- 写入模式:$([bool]$AllowWrite)") | 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.result_rows) |") | Out-Null +$md.Add("| 回填计划行 | $($metrics.apply_plan_rows) |") | Out-Null +$md.Add("| 阻塞项 | $($metrics.blockers) |") | Out-Null +$md.Add("| 仍待复核 | $($metrics.pending_reviews) |") | Out-Null +$md.Add("| 可进入试点复核库 | $($metrics.approved_for_review_import) |") | Out-Null +$md.Add("| 可进入生产发布 | $($metrics.approved_for_production) |") | Out-Null +$md.Add("| 需修改后复核 | $($metrics.changes_required) |") | Out-Null +$md.Add("| 已拒绝 | $($metrics.rejected) |") | Out-Null +$md.Add("") | Out-Null +$md.Add("## 检查项") | Out-Null +$md.Add("") | Out-Null +$md.Add("| 检查项 | 必需 | 状态 | 说明 |") | Out-Null +$md.Add("|---|---:|---|---|") | Out-Null +foreach ($check in $checks) { + $requiredText = if ($check.required) { "是" } else { "否" } + $md.Add("| $($check.label) | $requiredText | $(Convert-CheckStatusText -Status $check.status) | $($check.detail) |") | Out-Null +} +$md.Add("") | Out-Null +$md.Add("## 当前结论") | Out-Null +$md.Add("") | Out-Null +if ($overallStatus -eq "BLOCKED") { + $md.Add("复核结果回填被正确阻塞。当前结果中仍有 PENDING 或缺少真实复核证据的任务。") | Out-Null +} elseif ($overallStatus -eq "PASS") { + $md.Add("复核结果已通过回填门禁。若本次使用 DryRun,可在复核证据确认后显式使用 -AllowWrite 回填登记表。") | Out-Null +} else { + $md.Add("复核结果回填未完全通过,请查看阻塞清单。") | Out-Null +} +$md.Add("") | Out-Null +$md.Add("本地产物:") | Out-Null +$md.Add("") | Out-Null +$md.Add("- phase13-review-result-apply-summary.json") | Out-Null +$md.Add("- phase13-review-result-apply-summary.md") | Out-Null +$md.Add("- review-result-apply-plan.csv") | Out-Null +$md.Add("- review-result-apply-blockers.csv") | Out-Null +$md | Set-Content -Encoding UTF8 -Path $summaryMd + +Write-Host "PHASE13_REVIEW_RESULT_APPLY_$overallStatus $artifactDir" +if ($overallStatus -eq "FAIL") { + throw "阶段 13 复核结果回填门禁未通过。查看:$summaryMd" +} +if ($overallStatus -eq "BLOCKED" -and $FailOnBlocked) { + throw "阶段 13 复核结果回填门禁被阻塞。查看:$summaryMd" +} diff --git a/scripts/run-legal-knowledge-library-gate.ps1 b/scripts/run-legal-knowledge-library-gate.ps1 index 97ea359e..b11db8f6 100644 --- a/scripts/run-legal-knowledge-library-gate.ps1 +++ b/scripts/run-legal-knowledge-library-gate.ps1 @@ -285,6 +285,14 @@ function Test-SecretLeak { if (Test-Path -LiteralPath $reviewExecutionScriptPath -PathType Leaf) { $files += Get-Item -LiteralPath $reviewExecutionScriptPath } + $firstWaveReviewScriptPath = Join-Path $repoRoot "scripts\run-phase13-first-wave-review-readiness.ps1" + if (Test-Path -LiteralPath $firstWaveReviewScriptPath -PathType Leaf) { + $files += Get-Item -LiteralPath $firstWaveReviewScriptPath + } + $reviewResultApplyScriptPath = Join-Path $repoRoot "scripts\apply-phase13-review-results.ps1" + if (Test-Path -LiteralPath $reviewResultApplyScriptPath -PathType Leaf) { + $files += Get-Item -LiteralPath $reviewResultApplyScriptPath + } foreach ($file in $files) { $match = Select-String -LiteralPath $file.FullName -Pattern $adminPasswordLiteral -List -ErrorAction SilentlyContinue if ($match) { @@ -356,7 +364,9 @@ $commonDocFiles = @( "scripts\run-phase13-golden-eval.ps1", "scripts\run-phase13-ingestion-closed-loop.ps1", "scripts\run-phase13-production-pilot-release.ps1", - "scripts\run-phase13-review-execution-gate.ps1" + "scripts\run-phase13-review-execution-gate.ps1", + "scripts\run-phase13-first-wave-review-readiness.ps1", + "scripts\apply-phase13-review-results.ps1" ) foreach ($docPath in $commonDocFiles) { $absoluteDocPath = Join-Path $repoRoot $docPath diff --git a/scripts/run-phase13-first-wave-review-readiness.ps1 b/scripts/run-phase13-first-wave-review-readiness.ps1 new file mode 100644 index 00000000..07af2520 --- /dev/null +++ b/scripts/run-phase13-first-wave-review-readiness.ps1 @@ -0,0 +1,660 @@ +param( + [string]$LibraryRoot = "", + [string]$ArtifactRoot = "", + [ValidateSet("P0", "FirstWave", "All")] + [string]$Scope = "P0", + [string]$LegalReviewer = "", + [string]$BusinessReviewer = "", + [string]$DueDate = "", + [switch]$FailOnBlocked +) + +$ErrorActionPreference = "Stop" + +$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path +$timestamp = Get-Date -Format "yyyy-MM-ddTHH-mm-ss-fff" + +if ([string]::IsNullOrWhiteSpace($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-first-wave-review-readiness" +} +if (-not [System.IO.Path]::IsPathRooted($ArtifactRoot)) { + $ArtifactRoot = Join-Path $repoRoot $ArtifactRoot +} + +$artifactDir = Join-Path $ArtifactRoot $timestamp +$formsDir = Join-Path $artifactDir "review-forms" +New-Item -ItemType Directory -Force -Path $formsDir | Out-Null + +$summaryJson = Join-Path $artifactDir "phase13-first-wave-review-readiness-summary.json" +$summaryMd = Join-Path $artifactDir "phase13-first-wave-review-readiness-summary.md" +$assignmentCsv = Join-Path $artifactDir "review-assignment-template.csv" +$resultImportCsv = Join-Path $artifactDir "review-result-import-template.csv" +$selectedTasksCsv = Join-Path $artifactDir "selected-review-tasks.csv" +$checklistMd = Join-Path $artifactDir "first-wave-review-checklist.md" +$gitStatusPath = Join-Path $artifactDir "git-status.txt" + +$checks = New-Object System.Collections.Generic.List[object] +$selectedTasks = New-Object System.Collections.Generic.List[object] +$assignmentRows = New-Object System.Collections.Generic.List[object] +$resultRows = 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 Get-FieldValue { + param([object]$Row, [string]$Name) + if ($null -eq $Row) { + return "" + } + $property = $Row.PSObject.Properties[$Name] + if ($null -eq $property -or $null -eq $property.Value) { + return "" + } + return [string]$property.Value +} + +function Test-PendingReviewer { + param([string]$Value) + if ([string]::IsNullOrWhiteSpace($Value)) { + return $true + } + return @("pending", "待法务复核", "待业务复核", "待法务和业务复核", "待复核") -contains $Value.Trim() +} + +function Test-RequiredFlag { + param([string]$Value) + if ([string]::IsNullOrWhiteSpace($Value)) { + return $false + } + return @("true", "yes", "1", "required") -contains $Value.ToLowerInvariant() +} + +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-OverallStatusText { + param([string]$Status) + switch ($Status) { + "PASS" { return "通过" } + "WARN" { return "通过但有警告" } + "FAIL" { return "未通过" } + "BLOCKED" { return "阻塞" } + default { return $Status } + } +} + +function Convert-CheckStatusText { + param([string]$Status) + switch ($Status) { + "pass" { return "通过" } + "fail" { return "失败" } + "warn" { return "警告" } + "skip" { return "跳过" } + "blocked" { return "阻塞" } + default { return $Status } + } +} + +function Get-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 = "", + [string]$PrimaryPath = "", + [string]$SecondaryPath = "" + ) + + 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 + primary_path = $PrimaryPath + secondary_path = $SecondaryPath + } +} + +function Get-ProposedReviewerValue { + param([string]$Lane, [string]$CurrentReviewer) + if ($Lane -eq "法务+业务" -and -not [string]::IsNullOrWhiteSpace($LegalReviewer) -and -not [string]::IsNullOrWhiteSpace($BusinessReviewer)) { + return "$LegalReviewer;$BusinessReviewer" + } + if ($Lane -eq "法务" -and -not [string]::IsNullOrWhiteSpace($LegalReviewer)) { + return $LegalReviewer + } + return $CurrentReviewer +} + +function Write-ReviewForm { + param([object]$Task, [string]$Path) + $lines = New-Object System.Collections.Generic.List[string] + $lines.Add("# 阶段 13 首批复核表:$($Task.review_id)") | Out-Null + $lines.Add("") | Out-Null + $lines.Add("## 复核对象") | Out-Null + $lines.Add("") | Out-Null + $lines.Add("| 字段 | 值 |") | Out-Null + $lines.Add("|---|---|") | Out-Null + $lines.Add("| 任务编号 | `$($Task.review_id)` |") | Out-Null + $lines.Add("| 资产类型 | $($Task.asset_type_text) |") | Out-Null + $lines.Add("| 资产编号 | `$($Task.asset_id)` |") | Out-Null + $lines.Add("| 资产名称 | $($Task.asset_name) |") | Out-Null + $lines.Add("| 优先级 | $($Task.priority) |") | Out-Null + $lines.Add("| 复核通道 | $($Task.review_lane) |") | Out-Null + $lines.Add("| 法律依据 | `$($Task.source_ids)` |") | Out-Null + $lines.Add("| 主文件 | `$($Task.primary_path)` |") | Out-Null + $lines.Add("| 辅助文件 | `$($Task.secondary_path)` |") | Out-Null + $lines.Add("") | Out-Null + $lines.Add("## 法务复核") | Out-Null + $lines.Add("") | Out-Null + $lines.Add("1. 法源编号、条款引用、适用范围和风险提示是否准确。") | Out-Null + $lines.Add("2. 结论是否存在脱离中国现行法律依据的表述。") | Out-Null + $lines.Add("3. 高风险事项是否保留人工复核或专项复核提示。") | Out-Null + $lines.Add("4. 不得在模板或样本中固化未经业务确认的金额、比例、机构、地点、期限或权属安排。") | Out-Null + $lines.Add("") | Out-Null + $lines.Add("## 业务复核") | Out-Null + $lines.Add("") | Out-Null + $lines.Add("1. 变量、默认值、枚举值和必填项是否符合真实业务。") | Out-Null + $lines.Add("2. 付款、交付、验收、服务级别、数据范围、责任上限和知识产权安排是否需要业务确认。") | Out-Null + $lines.Add("3. 模板或样本是否适合试点业务使用,是否需要按部门、产品线或合同类型拆分版本。") | Out-Null + $lines.Add("") | Out-Null + $lines.Add("## 复核结论填写区") | Out-Null + $lines.Add("") | Out-Null + $lines.Add("| 字段 | 填写 |") | Out-Null + $lines.Add("|---|---|") | Out-Null + $lines.Add("| 复核人 | |") | Out-Null + $lines.Add("| 复核日期 | |") | Out-Null + $lines.Add("| 复核结果 | PENDING / CHANGES_REQUIRED / APPROVED_FOR_REVIEW_IMPORT / APPROVED_FOR_PRODUCTION / REJECTED |") | Out-Null + $lines.Add("| 修改要求 | |") | Out-Null + $lines.Add("| 是否允许进入试点复核库 | 是 / 否 |") | Out-Null + $lines.Add("| 是否允许进入生产发布 | 是 / 否 |") | Out-Null + $lines.Add("| 复核依据和说明 | |") | Out-Null + $lines | Set-Content -Encoding UTF8 -Path $Path +} + +if (-not (Test-Path "$HOME\.Codex\skills\gstack\bin")) { + Add-Check -Key "gstack" -Label "gstack 前置检查" -Status "fail" -Detail "gstack 缺失" +} else { + Add-Check -Key "gstack" -Label "gstack 前置检查" -Status "pass" -Detail "GSTACK_OK" +} + +$branch = (git -C $repoRoot rev-parse --abbrev-ref HEAD).Trim() +$commit = (git -C $repoRoot rev-parse HEAD).Trim() +$remoteUrl = (git -C $repoRoot remote get-url origin).Trim() +$gitStatus = git -C $repoRoot status --short +($gitStatus -join [Environment]::NewLine) | Set-Content -Encoding UTF8 -Path $gitStatusPath + +if ($remoteUrl -match "192\.168\.3\.90") { + Add-Check -Key "origin_remote" -Label "origin 远端仓库" -Status "pass" -Detail "origin 指向 192.168.3.90" -Meta @{ branch = $branch; commit = $commit; remote = $remoteUrl } +} else { + Add-Check -Key "origin_remote" -Label "origin 远端仓库" -Status "fail" -Detail "origin 未指向 192.168.3.90" -Meta @{ branch = $branch; commit = $commit; remote = $remoteUrl } +} + +if ($gitStatus) { + Add-Check -Key "working_tree" -Label "工作区状态" -Status "warn" -Required $false -Detail "工作区存在未提交改动;本次门禁按证据记录" -Meta @{ artifact = $gitStatusPath } +} else { + Add-Check -Key "working_tree" -Label "工作区状态" -Status "pass" -Detail "工作区干净" -Meta @{ artifact = $gitStatusPath } +} + +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" +$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 (Join-Path $inventoryRoot "expert-review-register.csv") + +$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 -PrimaryPath $row.retrieval_hint +} +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 -PrimaryPath $row.summary_path +} +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 -PrimaryPath $row.modification_suggestion_path -SecondaryPath $row.review_sample_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 -PrimaryPath $row.clause_path -SecondaryPath $row.risk_notes_path +} +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 $row.clause_ids -PrimaryPath $row.template_path -SecondaryPath $row.variable_schema_path +} +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 "" -Severity $row.expected_severity -SourceIds $row.source_ids -PrimaryPath $row.input_sample_path -SecondaryPath $row.expected_findings_path +} +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 -PrimaryPath $row.variable_payload_path -SecondaryPath $row.expected_document_path +} +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 +} + +foreach ($row in $reviewRows) { + $assetKey = "$($row.asset_type):$($row.asset_id)" + if (-not $assetMap.ContainsKey($assetKey)) { + continue + } + $asset = $assetMap[$assetKey] + $priority = Get-ReviewPriority -AssetType $row.asset_type -Asset $asset + $lane = Get-ReviewLane -AssetType $row.asset_type -ReviewerRole $row.reviewer_role + $include = $false + if ($Scope -eq "All") { + $include = $true + } elseif ($Scope -eq "FirstWave") { + $include = @("contract_template", "clause", "review_rule", "generation_golden_sample") -contains $row.asset_type + } elseif ($Scope -eq "P0") { + $include = ($priority -eq "P0") + } + if (-not $include) { + continue + } + + $formRelativePath = "review-forms/$($row.review_id).md" + $formPath = Join-Path $artifactDir ($formRelativePath -replace "/", "\") + $proposedReviewer = Get-ProposedReviewerValue -Lane $lane -CurrentReviewer $row.reviewer + $requiresBusiness = ($lane -eq "法务+业务") + + $task = [pscustomobject][ordered]@{ + priority = $priority + review_lane = $lane + review_id = $row.review_id + asset_type = $row.asset_type + asset_type_text = Get-AssetTypeText -AssetType $row.asset_type + asset_id = $row.asset_id + asset_name = $asset.asset_name + asset_status = $asset.status + production_ready = $asset.production_ready + source_ids = $asset.source_ids + severity = $asset.severity + primary_path = $asset.primary_path + primary_path_exists = if ([string]::IsNullOrWhiteSpace($asset.primary_path)) { $false } else { Test-LibraryFilePath -RelativePath $asset.primary_path } + secondary_path = $asset.secondary_path + current_reviewer = $row.reviewer + proposed_reviewer = $proposedReviewer + proposed_legal_reviewer = $LegalReviewer + proposed_business_reviewer = if ($requiresBusiness) { $BusinessReviewer } else { "" } + due_date = $DueDate + review_result = $row.review_result + review_status = $row.status + required_changes = $row.required_changes + follow_up_owner = $row.follow_up_owner + evidence_path = $row.evidence_path + evidence_form_path = $formRelativePath + notes = $row.notes + } + + $selectedTasks.Add($task) | Out-Null + Write-ReviewForm -Task $task -Path $formPath + + $assignmentRows.Add([pscustomobject][ordered]@{ + priority = $task.priority + review_lane = $task.review_lane + review_id = $task.review_id + asset_type = $task.asset_type + asset_type_text = $task.asset_type_text + asset_id = $task.asset_id + asset_name = $task.asset_name + current_reviewer = $task.current_reviewer + proposed_legal_reviewer = $task.proposed_legal_reviewer + proposed_business_reviewer = $task.proposed_business_reviewer + due_date = $task.due_date + review_result = $task.review_result + status = $task.review_status + required_changes = $task.required_changes + follow_up_owner = $task.follow_up_owner + evidence_form_path = $task.evidence_form_path + source_ids = $task.source_ids + primary_path = $task.primary_path + secondary_path = $task.secondary_path + notes = $task.notes + }) | Out-Null + + $resultRows.Add([pscustomobject][ordered]@{ + review_id = $task.review_id + asset_type = $task.asset_type + asset_id = $task.asset_id + reviewer = $task.proposed_reviewer + reviewer_role = $task.review_lane + review_date = "" + review_result = "PENDING" + status = "PENDING" + required_changes = "" + follow_up_owner = $task.follow_up_owner + evidence_path = $task.evidence_form_path + asset_status_after_review = $task.asset_status + production_ready_after_review = $task.production_ready + notes = "由真实法务或业务复核后填写;未复核不得改为 APPROVED 或 production_ready=true" + }) | Out-Null +} + +$selectedTasks | + Sort-Object priority, review_lane, asset_type, asset_id | + Export-Csv -LiteralPath $selectedTasksCsv -NoTypeInformation -Encoding UTF8 +$assignmentRows | + Sort-Object priority, review_lane, asset_type, asset_id | + Export-Csv -LiteralPath $assignmentCsv -NoTypeInformation -Encoding UTF8 +$resultRows | + Sort-Object review_id | + Export-Csv -LiteralPath $resultImportCsv -NoTypeInformation -Encoding UTF8 + +$missingTaskPaths = @($selectedTasks | Where-Object { -not $_.primary_path_exists } | ForEach-Object { $_.review_id }) +$missingLegalReviewer = @($selectedTasks | Where-Object { [string]::IsNullOrWhiteSpace($LegalReviewer) -or (Test-PendingReviewer -Value $LegalReviewer) } | ForEach-Object { $_.review_id }) +$missingBusinessReviewer = @($selectedTasks | Where-Object { $_.review_lane -eq "法务+业务" -and ([string]::IsNullOrWhiteSpace($BusinessReviewer) -or (Test-PendingReviewer -Value $BusinessReviewer)) } | ForEach-Object { $_.review_id }) +$missingDueDate = [string]::IsNullOrWhiteSpace($DueDate) +$invalidDueDate = $false +if (-not $missingDueDate) { + $parsedDueDate = [datetime]::MinValue + $invalidDueDate = -not [datetime]::TryParse($DueDate, [ref]$parsedDueDate) +} + +if ($selectedTasks.Count -gt 0) { + Add-Check -Key "selected_tasks" -Label "首批复核任务选择" -Status "pass" -Detail "已选择 $($selectedTasks.Count) 个 $Scope 复核任务" +} else { + Add-Check -Key "selected_tasks" -Label "首批复核任务选择" -Status "fail" -Detail "未选择到可复核任务" +} +if ($missingTaskPaths.Count -eq 0) { + Add-Check -Key "asset_file_paths" -Label "复核资产文件路径" -Status "pass" -Detail "所选复核任务均有可追溯主文件" +} else { + Add-Check -Key "asset_file_paths" -Label "复核资产文件路径" -Status "fail" -Detail "以下复核任务主文件缺失或不可验证:$($missingTaskPaths -join ',')" -Meta @{ rows = $missingTaskPaths } +} +if ($missingLegalReviewer.Count -eq 0) { + Add-Check -Key "legal_reviewer_assignment" -Label "法务复核人分配" -Status "pass" -Detail "首批复核任务已分配真实法务复核人" +} else { + Add-Check -Key "legal_reviewer_assignment" -Label "法务复核人分配" -Status "blocked" -Detail "缺少真实法务复核人:$($missingLegalReviewer.Count) 个任务" -Meta @{ rows = $missingLegalReviewer } +} +if ($missingBusinessReviewer.Count -eq 0) { + Add-Check -Key "business_reviewer_assignment" -Label "业务复核人分配" -Status "pass" -Detail "需要业务复核的任务已分配真实业务复核人" +} else { + Add-Check -Key "business_reviewer_assignment" -Label "业务复核人分配" -Status "blocked" -Detail "缺少真实业务复核人:$($missingBusinessReviewer.Count) 个任务" -Meta @{ rows = $missingBusinessReviewer } +} +if (-not $missingDueDate -and -not $invalidDueDate) { + Add-Check -Key "review_due_date" -Label "复核截止日期" -Status "pass" -Detail "首批复核截止日期=$DueDate" +} elseif ($invalidDueDate) { + Add-Check -Key "review_due_date" -Label "复核截止日期" -Status "fail" -Detail "复核截止日期无法解析:$DueDate" +} else { + Add-Check -Key "review_due_date" -Label "复核截止日期" -Status "blocked" -Detail "未设置首批复核截止日期" +} + +$p0Tasks = @($selectedTasks | Where-Object { $_.priority -eq "P0" }) +$templateTasks = @($selectedTasks | Where-Object { $_.asset_type -eq "contract_template" }) +$templateSampleTasks = @($selectedTasks | Where-Object { $_.asset_type -eq "generation_golden_sample" -and $_.asset_id -match "GEN-SAMPLE-TPL-" }) +$clauseTasks = @($selectedTasks | Where-Object { $_.asset_type -eq "clause" }) +$highRiskRuleTasks = @($selectedTasks | Where-Object { $_.asset_type -eq "review_rule" -and $_.severity -eq "high" }) +$legalBusinessTasks = @($selectedTasks | Where-Object { $_.review_lane -eq "法务+业务" }) +$legalOnlyTasks = @($selectedTasks | Where-Object { $_.review_lane -eq "法务" }) + +$checklist = New-Object System.Collections.Generic.List[string] +$checklist.Add("# 阶段 13 首批 P0 复核包") | Out-Null +$checklist.Add("") | Out-Null +$checklist.Add("本复核包由 scripts/run-phase13-first-wave-review-readiness.ps1 生成,用于把首批 P0 资料交给真实法务和业务负责人复核。") | Out-Null +$checklist.Add("") | Out-Null +$checklist.Add("## 使用顺序") | Out-Null +$checklist.Add("") | Out-Null +$checklist.Add("1. 在 review-assignment-template.csv 中确认复核人和截止日期。") | Out-Null +$checklist.Add("2. 按 review-forms/ 中的单项复核表完成法务和业务意见。") | Out-Null +$checklist.Add("3. 在 review-result-import-template.csv 中填写复核日期、复核结果、修改要求和证据路径。") | Out-Null +$checklist.Add("4. 只有真实复核完成后,才允许把相应资产推进到 APPROVED 或 production_ready=true。") | Out-Null +$checklist.Add("5. 回填后重新运行复核执行门禁和生产试点发布门禁。") | Out-Null +$checklist.Add("") | Out-Null +$checklist.Add("## 自动化复核命令") | Out-Null +$checklist.Add("") | Out-Null +$checklist.Add('```powershell') | Out-Null +$checklist.Add("powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts\run-phase13-first-wave-review-readiness.ps1 -LegalReviewer <法务姓名> -BusinessReviewer <业务姓名> -DueDate ") | Out-Null +$checklist.Add("powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts\run-phase13-review-execution-gate.ps1") | Out-Null +$checklist.Add("powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts\run-phase13-production-pilot-release.ps1") | Out-Null +$checklist.Add('```') | Out-Null +$checklist.Add("") | Out-Null +$checklist.Add("## 首批范围") | Out-Null +$checklist.Add("") | Out-Null +$checklist.Add("| 类型 | 数量 |") | Out-Null +$checklist.Add("|---|---:|") | Out-Null +$checklist.Add("| P0 任务 | $($p0Tasks.Count) |") | Out-Null +$checklist.Add("| 合同模板 | $($templateTasks.Count) |") | Out-Null +$checklist.Add("| 模板生成样本 | $($templateSampleTasks.Count) |") | Out-Null +$checklist.Add("| 标准条款 | $($clauseTasks.Count) |") | Out-Null +$checklist.Add("| 高风险审核规则 | $($highRiskRuleTasks.Count) |") | Out-Null +$checklist | Set-Content -Encoding UTF8 -Path $checklistMd + +$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]@{ + scope = $Scope + selected_tasks = $selectedTasks.Count + p0_tasks = $p0Tasks.Count + template_tasks = $templateTasks.Count + template_generation_sample_tasks = $templateSampleTasks.Count + clause_tasks = $clauseTasks.Count + high_risk_review_rule_tasks = $highRiskRuleTasks.Count + legal_only_tasks = $legalOnlyTasks.Count + legal_business_tasks = $legalBusinessTasks.Count + missing_legal_reviewer_tasks = $missingLegalReviewer.Count + missing_business_reviewer_tasks = $missingBusinessReviewer.Count + due_date_missing = $missingDueDate + due_date_invalid = $invalidDueDate +} + +$summary = [ordered]@{ + name = "phase13-first-wave-review-readiness" + status = $overallStatus + generated_at = (Get-Date).ToUniversalTime().ToString("o") + branch = $branch + commit = $commit + remote = $remoteUrl + library_root = $LibraryRoot + scope = $Scope + checks = $checks.ToArray() + metrics = $metrics + artifacts = [ordered]@{ + summary_json = $summaryJson + summary_md = $summaryMd + assignment_template = $assignmentCsv + result_import_template = $resultImportCsv + selected_tasks = $selectedTasksCsv + checklist = $checklistMd + review_forms_dir = $formsDir + git_status = $gitStatusPath + } + totals = [ordered]@{ + total_checks = $checks.Count + failed_required = $failedRequired + blocked_required = $blockedRequired + warnings = $warnings + } +} +Save-Json -Path $summaryJson -Value $summary + +$md = New-Object System.Collections.Generic.List[string] +$md.Add("# 阶段 13 首批复核就绪门禁") | Out-Null +$md.Add("") | Out-Null +$md.Add("- 门禁状态:$(Convert-OverallStatusText -Status $overallStatus)($overallStatus)") | Out-Null +$md.Add("- 生成时间:$($summary.generated_at)") | Out-Null +$md.Add("- 分支:$branch") | Out-Null +$md.Add("- 提交:$commit") | Out-Null +$md.Add("- 复核范围:$Scope") | 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.selected_tasks) |") | Out-Null +$md.Add("| P0 任务 | $($metrics.p0_tasks) |") | Out-Null +$md.Add("| 合同模板 | $($metrics.template_tasks) |") | Out-Null +$md.Add("| 模板生成样本 | $($metrics.template_generation_sample_tasks) |") | Out-Null +$md.Add("| 标准条款 | $($metrics.clause_tasks) |") | Out-Null +$md.Add("| 高风险审核规则 | $($metrics.high_risk_review_rule_tasks) |") | Out-Null +$md.Add("| 法务通道任务 | $($metrics.legal_only_tasks) |") | Out-Null +$md.Add("| 法务+业务通道任务 | $($metrics.legal_business_tasks) |") | Out-Null +$md.Add("| 缺少真实法务复核人任务 | $($metrics.missing_legal_reviewer_tasks) |") | Out-Null +$md.Add("| 缺少真实业务复核人任务 | $($metrics.missing_business_reviewer_tasks) |") | Out-Null +$md.Add("") | Out-Null +$md.Add("## 检查项") | Out-Null +$md.Add("") | Out-Null +$md.Add("| 检查项 | 必需 | 状态 | 说明 |") | Out-Null +$md.Add("|---|---:|---|---|") | Out-Null +foreach ($check in $checks) { + $requiredText = if ($check.required) { "是" } else { "否" } + $md.Add("| $($check.label) | $requiredText | $(Convert-CheckStatusText -Status $check.status) | $($check.detail) |") | Out-Null +} +$md.Add("") | Out-Null +$md.Add("## 当前结论") | Out-Null +$md.Add("") | Out-Null +if ($overallStatus -eq "BLOCKED") { + $md.Add("首批复核包已生成,但尚未分配真实法务/业务复核人或截止日期,不能视为复核执行完成。") | Out-Null +} elseif ($overallStatus -eq "PASS") { + $md.Add("首批复核包已具备执行条件,可以交给真实法务和业务负责人进行复核。") | Out-Null +} else { + $md.Add("首批复核包未完全就绪,请查看检查项和产物。") | Out-Null +} +$md.Add("") | Out-Null +$md.Add("本地产物:") | Out-Null +$md.Add("") | Out-Null +$md.Add("- phase13-first-wave-review-readiness-summary.json") | Out-Null +$md.Add("- phase13-first-wave-review-readiness-summary.md") | Out-Null +$md.Add("- review-assignment-template.csv") | Out-Null +$md.Add("- review-result-import-template.csv") | Out-Null +$md.Add("- selected-review-tasks.csv") | Out-Null +$md.Add("- first-wave-review-checklist.md") | Out-Null +$md.Add("- review-forms/") | Out-Null +$md | Set-Content -Encoding UTF8 -Path $summaryMd + +Write-Host "PHASE13_FIRST_WAVE_REVIEW_READINESS_$overallStatus $artifactDir" +if ($overallStatus -eq "FAIL") { + throw "阶段 13 首批复核就绪门禁未通过。查看:$summaryMd" +} +if ($overallStatus -eq "BLOCKED" -and $FailOnBlocked) { + throw "阶段 13 首批复核就绪门禁被阻塞。查看:$summaryMd" +}