From f5cf88f7d204b8ef2a13fb553f130c6ad4eccbef Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 1 May 2026 22:57:26 +0800 Subject: [PATCH] docs: add phase 13 pilot release gate --- .../README.md | 3 +- ...¾‹知识库与合同模板库建设闭环方案-2026-05-01.md | 2 + ...“与合同模板库建设闭环第七批执行记录-2026-05-01.md | 94 +++ scripts/run-legal-knowledge-library-gate.ps1 | 7 +- .../run-phase13-production-pilot-release.ps1 | 638 ++++++++++++++++++ 5 files changed, 742 insertions(+), 2 deletions(-) create mode 100644 project-docs/阶段13-法律知识库与合同模板库建设闭环第七批执行记录-2026-05-01.md create mode 100644 scripts/run-phase13-production-pilot-release.ps1 diff --git a/project-docs/phase13-legal-knowledge-template-library/README.md b/project-docs/phase13-legal-knowledge-template-library/README.md index ab634604..81985455 100644 --- a/project-docs/phase13-legal-knowledge-template-library/README.md +++ b/project-docs/phase13-legal-knowledge-template-library/README.md @@ -33,4 +33,5 @@ 7. 阶段 13.5 黄金样本评测脚本已建立,可离线评测审核样本、生成样本和知识库问答样本。 8. 阶段 13.7 一键导入脚本已建立,默认 `Validate` 模式不改系统,`Review` 模式可导入待法务复核知识库和审核规则,`Production` 模式只导入已复核且生产就绪资料。 9. 阶段 13.7 入库闭环总控脚本已建立,可一键串联黄金样本评测、资料门禁和导入校验,并输出闭环证据包。 -10. 生产发布仍需等待法务复核和业务复核完成。 +10. 阶段 13.8 生产试点发布门禁脚本已建立,默认 `DryRun` 模式只做发布前验证,并在资料未复核或模板库缺失时输出阻塞证据。 +11. 生产发布仍需等待法务复核、业务复核和合同模板库补齐完成。 diff --git a/project-docs/阶段13-法律知识库与合同模板库建设闭环方案-2026-05-01.md b/project-docs/阶段13-法律知识库与合同模板库建设闭环方案-2026-05-01.md index 472f55ed..cfec3317 100644 --- a/project-docs/阶段13-法律知识库与合同模板库建设闭环方案-2026-05-01.md +++ b/project-docs/阶段13-法律知识库与合同模板库建设闭环方案-2026-05-01.md @@ -402,3 +402,5 @@ project-docs/phase13-legal-knowledge-template-library/ 当前第二批执行结果:`13.2` 第一批中文法律知识卡已建立,共 `10` 张;`13.7` 第一版一键导入脚本 `scripts/import-phase13-library.ps1` 已落地。导入脚本支持 `Validate`、`Review`、`Production` 三种模式,默认不改系统;生产导入只允许已复核且生产就绪资料。门禁状态仍为 `PASS_WITH_LEGAL_REVIEW_REQUIRED`,因为法源和知识卡均需法务复核后才能成为生产确定性依据。 当前第六批执行结果:`13.7` 入库与自动化门禁闭环总控脚本 `scripts/run-phase13-ingestion-closed-loop.ps1` 已落地,可一键串联黄金样本评测、资料包门禁和导入校验。默认 `Validate` 模式不写入系统 API;真实 `Review` 或 `Production` 入库必须显式传入 `-AllowApiImport`。闭环状态为 `PASS_WITH_LEGAL_REVIEW_REQUIRED`,阻塞项仍是法务复核、业务复核和完整合同模板库建设。 + +当前第七批执行结果:`13.8` 生产试点发布门禁脚本 `scripts/run-phase13-production-pilot-release.ps1` 已落地。脚本默认 `DryRun` 模式不写系统 API,会串联入库闭环并输出发布候选资产表和阻塞资产表;真实 `Review` 或 `Production` 发布必须显式传入 `-AllowApiRelease`。当前发布状态为 `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..16a20537 --- /dev/null +++ b/project-docs/阶段13-法律知识库与合同模板库建设闭环第七批执行记录-2026-05-01.md @@ -0,0 +1,94 @@ +# AI合同平台 阶段 13 法律知识库与合同模板库建设闭环第七批执行记录(2026-05-01) + +## 一、阶段结论 + +本批次进入 `13.8 生产试点发布`,当前状态为 `BLOCKED`。 + +这个 `BLOCKED` 是符合预期的发布门禁结果,不是脚本失败。阶段 13.7 的入库与自动化门禁已经能够跑通,但生产试点发布要求资料已经完成法务和业务复核,并且合同模板能够预览、生成、下载和审核。当前资料包没有任何已复核可发布资产,合同模板清单也为空,因此不能执行真实生产或试点发布。 + +## 二、本批次交付 + +新增脚本: + +1. `scripts/run-phase13-production-pilot-release.ps1` + +更新资料: + +1. `scripts/run-legal-knowledge-library-gate.ps1` +2. `project-docs/phase13-legal-knowledge-template-library/README.md` +3. `project-docs/阶段13-法律知识库与合同模板库建设闭环方案-2026-05-01.md` + +## 三、发布闭环设计 + +生产试点发布脚本默认采用 `DryRun` 模式,不写入系统 API。它负责完成以下闭环: + +1. 检查 gstack、内网远端、资料包和工作区状态。 +2. 串联 `scripts/run-phase13-ingestion-closed-loop.ps1`,确认入库前自动化门禁仍可运行。 +3. 扫描法源、知识卡、审核规则、标准条款、合同模板和黄金样本清单。 +4. 输出 `release-candidates.csv`,列出所有发布候选资产。 +5. 输出 `blocked-release-items.csv`,列出所有阻塞资产及阻塞原因。 +6. 对合同模板清单、可发布法源、可发布知识卡、可发布审核规则、可发布标准条款、可发布合同模板、模板生成样本覆盖和审核规则样本覆盖执行发布门禁。 + +真实 `Review` 或 `Production` 发布必须显式传入 `-AllowApiRelease`,避免未复核资料被误写入试点或生产环境。 + +## 四、自动化发布闭环结果 + +本批次闭环命令: + +```text +powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts\run-phase13-production-pilot-release.ps1 +``` + +闭环输出: + +```text +PHASE13_PRODUCTION_PILOT_RELEASE_BLOCKED D:\ai-contract-platform\project-docs\qa-artifacts\phase13-production-pilot-release\2026-05-01T22-56-24-065 +``` + +子门禁结果: + +| 子门禁 | 状态 | 产物 | +|---|---|---| +| 入库与自动化门禁闭环 | PASS_WITH_LEGAL_REVIEW_REQUIRED | `project-docs/qa-artifacts/phase13-ingestion-closed-loop/2026-05-01T22-56-25-887` | + +发布统计: + +| 指标 | 结果 | +|---|---:| +| 法源 | 20 | +| 知识卡 | 10 | +| 审核规则 | 8 | +| 标准条款 | 8 | +| 合同模板 | 0 | +| 审核样本 | 8 | +| 生成样本 | 8 | +| KB 问答样本 | 10 | +| 发布候选资产 | 72 | +| 可发布资产 | 0 | +| 阻塞资产 | 72 | +| 可发布模板 | 0 | + +## 五、发布阻塞项 + +| 阻塞项 | 状态 | 说明 | +|---|---|---| +| 合同模板清单 | BLOCKED | 模板清单为空,无法完成模板预览、生成、下载和审核验证 | +| 可发布法源 | BLOCKED | 没有已复核并可发布的法源 | +| 可发布知识卡 | BLOCKED | 没有已复核并可发布的知识卡 | +| 可发布审核规则 | BLOCKED | 没有已复核并可发布的审核规则 | +| 可发布标准条款 | BLOCKED | 没有已复核并可发布的标准条款 | +| 可发布合同模板 | BLOCKED | 没有已复核并可发布的合同模板 | +| 模板生成样本覆盖 | BLOCKED | 模板清单为空,无法验证生成样本覆盖 | + +## 六、当前结论 + +阶段 13.8 的自动化发布门禁已经建立并可复跑,但真实生产试点发布不能继续执行。当前系统拥有法律资料和评测样本的工程化底座,但尚未拥有可发布的法务复核结果和合同模板库。 + +下一步不应强行发布,而应进入 `合同模板库补齐 + 法务复核登记`: + +1. 建立第一批中文合同模板,至少覆盖采购合同、服务合同、劳动合同、保密协议和数据处理协议。 +2. 每个模板绑定标准条款、审核规则、变量结构、生成样本和预期审核结果。 +3. 将法源、知识卡、规则、条款、模板和样本的复核任务登记到 `expert-review-register.csv`。 +4. 完成复核后再运行 `scripts/run-phase13-production-pilot-release.ps1`,直到发布状态从 `BLOCKED` 变为 `PASS`。 + +阶段 13 第七批当前状态:`BLOCKED`。 diff --git a/scripts/run-legal-knowledge-library-gate.ps1 b/scripts/run-legal-knowledge-library-gate.ps1 index 7da369cd..2582dd5a 100644 --- a/scripts/run-legal-knowledge-library-gate.ps1 +++ b/scripts/run-legal-knowledge-library-gate.ps1 @@ -277,6 +277,10 @@ function Test-SecretLeak { if (Test-Path -LiteralPath $closedLoopScriptPath -PathType Leaf) { $files += Get-Item -LiteralPath $closedLoopScriptPath } + $pilotReleaseScriptPath = Join-Path $repoRoot "scripts\run-phase13-production-pilot-release.ps1" + if (Test-Path -LiteralPath $pilotReleaseScriptPath -PathType Leaf) { + $files += Get-Item -LiteralPath $pilotReleaseScriptPath + } foreach ($file in $files) { $match = Select-String -LiteralPath $file.FullName -Pattern $adminPasswordLiteral -List -ErrorAction SilentlyContinue if ($match) { @@ -346,7 +350,8 @@ $commonDocFiles = @( "project-docs\phase13-legal-knowledge-template-library\field-dictionary.md", "scripts\import-phase13-library.ps1", "scripts\run-phase13-golden-eval.ps1", - "scripts\run-phase13-ingestion-closed-loop.ps1" + "scripts\run-phase13-ingestion-closed-loop.ps1", + "scripts\run-phase13-production-pilot-release.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 new file mode 100644 index 00000000..98bd097b --- /dev/null +++ b/scripts/run-phase13-production-pilot-release.ps1 @@ -0,0 +1,638 @@ +param( + [ValidateSet("DryRun", "Review", "Production")] + [string]$ReleaseMode = "DryRun", + [string]$ApiBaseUrl = "http://127.0.0.1:8000/api/v1", + [string]$AdminEmail = "admin@example.com", + [string]$AdminPassword = "", + [string]$LibraryRoot = "", + [string]$ArtifactRoot = "", + [switch]$AllowApiRelease, + [switch]$ForceReupload, + [switch]$SkipTemplates, + [switch]$SkipReviewRules, + [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-production-pilot-release" +} +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-production-pilot-release-summary.json" +$summaryMd = Join-Path $artifactDir "phase13-production-pilot-release-summary.md" +$gitStatusPath = Join-Path $artifactDir "git-status.txt" +$candidateCsv = Join-Path $artifactDir "release-candidates.csv" +$blockedCsv = Join-Path $artifactDir "blocked-release-items.csv" + +$ApiBaseUrl = $ApiBaseUrl.TrimEnd("/") +$checks = New-Object System.Collections.Generic.List[object] +$childRuns = New-Object System.Collections.Generic.List[object] +$releaseCandidates = 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-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 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-ReleaseModeImportMode { + if ($ReleaseMode -eq "Production") { + return "Production" + } + if ($ReleaseMode -eq "Review") { + return "Review" + } + return "Validate" +} + +function Get-ChildSummaryPath { + param([string]$ChildArtifactDir, [string]$SummaryFileName) + if ([string]::IsNullOrWhiteSpace($ChildArtifactDir) -or [string]::IsNullOrWhiteSpace($SummaryFileName)) { + return "" + } + $candidate = Join-Path $ChildArtifactDir $SummaryFileName + if (Test-Path -LiteralPath $candidate -PathType Leaf) { + return $candidate + } + return "" +} + +function Read-JsonFile { + param([string]$Path) + if ([string]::IsNullOrWhiteSpace($Path) -or -not (Test-Path -LiteralPath $Path -PathType Leaf)) { + return $null + } + return Get-Content -LiteralPath $Path -Raw -Encoding UTF8 | ConvertFrom-Json +} + +function Invoke-StageScript { + param( + [string]$Key, + [string]$Label, + [string]$ScriptRelativePath, + [string[]]$Arguments, + [string]$ExpectedPrefix, + [string]$SummaryFileName + ) + + $scriptPath = Join-Path $repoRoot $ScriptRelativePath + $safeKey = $Key -replace "[^A-Za-z0-9_-]+", "-" + $logPath = Join-Path $artifactDir "$safeKey-output.log" + + if (-not (Test-Path -LiteralPath $scriptPath -PathType Leaf)) { + Add-Check -Key $Key -Label $Label -Status "fail" -Detail "脚本缺失:$ScriptRelativePath" -Meta @{ script = $scriptPath; output_log = $logPath } + return [pscustomobject]@{ key = $Key; label = $Label; child_status = "MISSING"; step_status = "fail"; artifact_dir = ""; summary_path = ""; output_log = $logPath; exit_code = 1; summary = $null } + } + + $argumentList = @("-NoProfile", "-ExecutionPolicy", "Bypass", "-File", $scriptPath) + $argumentList += $Arguments + + $output = & powershell.exe @argumentList 2>&1 + $exitCode = if ($null -eq $LASTEXITCODE) { 0 } else { $LASTEXITCODE } + $outputText = (@($output | ForEach-Object { [string]$_ }) -join [Environment]::NewLine) + $outputText | Set-Content -Encoding UTF8 -Path $logPath + + $status = "UNKNOWN" + $childArtifactDir = "" + $pattern = "(?m)^" + [regex]::Escape($ExpectedPrefix) + "(?[A-Z_]+)\s+(?.+?)\s*$" + $match = [regex]::Match($outputText, $pattern) + if ($match.Success) { + $status = $match.Groups["status"].Value + $childArtifactDir = $match.Groups["artifact"].Value.Trim() + } + + $summaryPath = Get-ChildSummaryPath -ChildArtifactDir $childArtifactDir -SummaryFileName $SummaryFileName + $childSummary = Read-JsonFile -Path $summaryPath + + $stepStatus = "pass" + $detail = "子门禁状态=$status" + if ($exitCode -ne 0) { + $stepStatus = "fail" + $detail = "子脚本退出码=$exitCode,状态=$status" + } elseif (-not $match.Success) { + $stepStatus = "fail" + $detail = "未识别到子门禁输出前缀:$ExpectedPrefix" + } elseif ($status -eq "FAIL") { + $stepStatus = "fail" + $detail = "子门禁状态=FAIL" + } elseif ($status -eq "WARN") { + $stepStatus = "warn" + $detail = "子门禁状态=WARN" + } elseif ($status -eq "BLOCKED") { + $stepStatus = "blocked" + $detail = "子门禁状态=BLOCKED" + } + + Add-Check -Key $Key -Label $Label -Status $stepStatus -Detail $detail -Meta @{ + child_status = $status + script = $scriptPath + artifact_dir = $childArtifactDir + summary_path = $summaryPath + output_log = $logPath + exit_code = $exitCode + } + + $result = [pscustomobject]@{ + key = $Key + label = $Label + child_status = $status + step_status = $stepStatus + artifact_dir = $childArtifactDir + summary_path = $summaryPath + output_log = $logPath + exit_code = $exitCode + summary = $childSummary + } + $childRuns.Add($result) | Out-Null + return $result +} + +function New-ReleaseCandidate { + param( + [string]$AssetType, + [object]$Row, + [string]$IdField, + [string]$NameField, + [bool]$RequireProductionReady = $true, + [bool]$RequireLegalReviewer = $true, + [bool]$RequireBusinessReviewer = $false, + [bool]$RequireValidityConfirmed = $false + ) + + $assetId = Get-FieldValue -Row $Row -Name $IdField + $assetName = Get-FieldValue -Row $Row -Name $NameField + $status = Get-FieldValue -Row $Row -Name "status" + $productionReady = Test-RequiredFlag -Value (Get-FieldValue -Row $Row -Name "production_ready") + $legalReviewer = Get-FieldValue -Row $Row -Name "legal_reviewer" + $businessReviewer = Get-FieldValue -Row $Row -Name "business_reviewer" + $validityStatus = Get-FieldValue -Row $Row -Name "validity_status" + $reasons = New-Object System.Collections.Generic.List[string] + + if (@("APPROVED", "PUBLISHED") -notcontains $status) { + $reasons.Add("status_not_approved:$status") | Out-Null + } + if ($RequireProductionReady -and -not $productionReady) { + $reasons.Add("production_ready_false") | Out-Null + } + if ($RequireLegalReviewer -and (Test-PendingReviewer -Value $legalReviewer)) { + $reasons.Add("legal_review_pending") | Out-Null + } + if ($RequireBusinessReviewer -and (Test-PendingReviewer -Value $businessReviewer)) { + $reasons.Add("business_review_pending") | Out-Null + } + if ($RequireValidityConfirmed -and ([string]::IsNullOrWhiteSpace($validityStatus) -or $validityStatus -eq "VERIFY_REQUIRED")) { + $reasons.Add("validity_status_unconfirmed") | Out-Null + } + + return [pscustomobject][ordered]@{ + asset_type = $AssetType + asset_id = $assetId + asset_name = $assetName + status = $status + production_ready = $productionReady + legal_reviewer = $legalReviewer + business_reviewer = $businessReviewer + validity_status = $validityStatus + release_ready = ($reasons.Count -eq 0) + block_reasons = ($reasons -join ";") + } +} + +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") + +foreach ($source in $legalSources) { + $releaseCandidates.Add((New-ReleaseCandidate -AssetType "legal_source" -Row $source -IdField "source_id" -NameField "title" -RequireProductionReady $false -RequireLegalReviewer $true -RequireValidityConfirmed $true)) | Out-Null +} +foreach ($card in $knowledgeCards) { + $releaseCandidates.Add((New-ReleaseCandidate -AssetType "knowledge_card" -Row $card -IdField "knowledge_card_id" -NameField "title")) | Out-Null +} +foreach ($rule in $reviewRules) { + $releaseCandidates.Add((New-ReleaseCandidate -AssetType "review_rule" -Row $rule -IdField "rule_id" -NameField "risk_name")) | Out-Null +} +foreach ($clause in $clauses) { + $releaseCandidates.Add((New-ReleaseCandidate -AssetType "clause" -Row $clause -IdField "clause_id" -NameField "clause_name" -RequireBusinessReviewer $true)) | Out-Null +} +foreach ($template in $templates) { + $releaseCandidates.Add((New-ReleaseCandidate -AssetType "contract_template" -Row $template -IdField "template_id" -NameField "template_name" -RequireBusinessReviewer $true)) | Out-Null +} +foreach ($sample in $reviewSamples) { + $releaseCandidates.Add((New-ReleaseCandidate -AssetType "review_golden_sample" -Row $sample -IdField "sample_id" -NameField "contract_type")) | Out-Null +} +foreach ($sample in $generationSamples) { + $releaseCandidates.Add((New-ReleaseCandidate -AssetType "generation_golden_sample" -Row $sample -IdField "sample_id" -NameField "contract_type" -RequireBusinessReviewer $true)) | Out-Null +} +foreach ($sample in $kbQaSamples) { + $releaseCandidates.Add((New-ReleaseCandidate -AssetType "kb_qa_golden_sample" -Row $sample -IdField "sample_id" -NameField "question")) | Out-Null +} + +$releaseCandidates | Export-Csv -LiteralPath $candidateCsv -NoTypeInformation -Encoding UTF8 +$blockedItems = @($releaseCandidates | Where-Object { -not $_.release_ready }) +$blockedItems | Export-Csv -LiteralPath $blockedCsv -NoTypeInformation -Encoding UTF8 + +$importMode = Get-ReleaseModeImportMode +$ingestionArgs = @( + "-ImportMode", $importMode, + "-LibraryRoot", $LibraryRoot, + "-ApiBaseUrl", $ApiBaseUrl, + "-AdminEmail", $AdminEmail +) +if ($ReleaseMode -ne "DryRun" -and $AllowApiRelease) { + $ingestionArgs += "-AllowApiImport" +} +if (-not [string]::IsNullOrWhiteSpace($AdminPassword)) { + $ingestionArgs += @("-AdminPassword", $AdminPassword) +} +if ($ForceReupload) { + $ingestionArgs += "-ForceReupload" +} +if ($SkipTemplates) { + $ingestionArgs += "-SkipTemplates" +} +if ($SkipReviewRules) { + $ingestionArgs += "-SkipReviewRules" +} + +$ingestionRun = Invoke-StageScript ` + -Key "ingestion_closed_loop" ` + -Label "入库与自动化门禁闭环" ` + -ScriptRelativePath "scripts\run-phase13-ingestion-closed-loop.ps1" ` + -Arguments $ingestionArgs ` + -ExpectedPrefix "PHASE13_INGESTION_CLOSED_LOOP_" ` + -SummaryFileName "phase13-ingestion-closed-loop-summary.json" + +if ($ReleaseMode -eq "DryRun") { + Add-Check -Key "api_release_guard" -Label "真实 API 发布保护" -Status "skip" -Required $false -Detail "DryRun 模式不写入系统 API" +} elseif ($AllowApiRelease) { + Add-Check -Key "api_release_guard" -Label "真实 API 发布保护" -Status "pass" -Detail "已显式允许 $ReleaseMode 模式写入 API" -Meta @{ release_mode = $ReleaseMode; api_base_url = $ApiBaseUrl } +} else { + Add-Check -Key "api_release_guard" -Label "真实 API 发布保护" -Status "blocked" -Detail "Review/Production 发布必须显式传入 -AllowApiRelease" -Meta @{ release_mode = $ReleaseMode; api_base_url = $ApiBaseUrl } +} + +$readyByType = @{} +foreach ($type in @("legal_source", "knowledge_card", "review_rule", "clause", "contract_template", "review_golden_sample", "generation_golden_sample", "kb_qa_golden_sample")) { + $readyByType[$type] = @($releaseCandidates | Where-Object { $_.asset_type -eq $type -and $_.release_ready }).Count +} + +if ($templates.Count -gt 0) { + Add-Check -Key "template_inventory" -Label "合同模板清单" -Status "pass" -Detail "合同模板数量=$($templates.Count)" +} else { + Add-Check -Key "template_inventory" -Label "合同模板清单" -Status "blocked" -Detail "合同模板清单为空,无法完成模板预览、生成、下载和审核验证" -Meta @{ inventory = (Join-Path $inventoryRoot "contract-template-inventory.csv") } +} + +if ($readyByType["legal_source"] -gt 0) { + Add-Check -Key "approved_legal_sources" -Label "可发布法源" -Status "pass" -Detail "可发布法源数量=$($readyByType["legal_source"])" +} else { + Add-Check -Key "approved_legal_sources" -Label "可发布法源" -Status "blocked" -Detail "没有已复核并可发布的法源" +} + +if ($readyByType["knowledge_card"] -gt 0) { + Add-Check -Key "approved_knowledge_cards" -Label "可发布知识卡" -Status "pass" -Detail "可发布知识卡数量=$($readyByType["knowledge_card"])" +} else { + Add-Check -Key "approved_knowledge_cards" -Label "可发布知识卡" -Status "blocked" -Detail "没有已复核并可发布的知识卡" +} + +if ($readyByType["review_rule"] -gt 0) { + Add-Check -Key "approved_review_rules" -Label "可发布审核规则" -Status "pass" -Detail "可发布审核规则数量=$($readyByType["review_rule"])" +} else { + Add-Check -Key "approved_review_rules" -Label "可发布审核规则" -Status "blocked" -Detail "没有已复核并可发布的审核规则" +} + +if ($readyByType["clause"] -gt 0) { + Add-Check -Key "approved_clauses" -Label "可发布标准条款" -Status "pass" -Detail "可发布标准条款数量=$($readyByType["clause"])" +} else { + Add-Check -Key "approved_clauses" -Label "可发布标准条款" -Status "blocked" -Detail "没有已复核并可发布的标准条款" +} + +if ($readyByType["contract_template"] -gt 0) { + Add-Check -Key "approved_templates" -Label "可发布合同模板" -Status "pass" -Detail "可发布合同模板数量=$($readyByType["contract_template"])" +} else { + Add-Check -Key "approved_templates" -Label "可发布合同模板" -Status "blocked" -Detail "没有已复核并可发布的合同模板" +} + +$templateIds = @{} +foreach ($template in $templates) { + $templateId = Get-FieldValue -Row $template -Name "template_id" + if (-not [string]::IsNullOrWhiteSpace($templateId)) { + $templateIds[$templateId] = $true + } +} +$generationSampleTemplateIds = @{} +foreach ($sample in $generationSamples) { + $sampleTemplateId = Get-FieldValue -Row $sample -Name "template_id" + if (-not [string]::IsNullOrWhiteSpace($sampleTemplateId)) { + $generationSampleTemplateIds[$sampleTemplateId] = $true + } +} +$templatesWithoutGenerationSamples = @($templateIds.Keys | Where-Object { -not $generationSampleTemplateIds.ContainsKey($_) }) +if ($templates.Count -eq 0) { + Add-Check -Key "template_generation_coverage" -Label "模板生成样本覆盖" -Status "blocked" -Detail "模板清单为空,无法验证生成样本覆盖" +} elseif ($templatesWithoutGenerationSamples.Count -eq 0) { + Add-Check -Key "template_generation_coverage" -Label "模板生成样本覆盖" -Status "pass" -Detail "所有模板均绑定生成样本" +} else { + Add-Check -Key "template_generation_coverage" -Label "模板生成样本覆盖" -Status "blocked" -Detail "以下模板缺少生成样本:$($templatesWithoutGenerationSamples -join ',')" -Meta @{ templates = $templatesWithoutGenerationSamples } +} + +$reviewRulesWithoutSamples = @($reviewRules | Where-Object { (Split-SemicolonValues -Value (Get-FieldValue -Row $_ -Name "review_sample_ids")).Count -eq 0 } | ForEach-Object { Get-FieldValue -Row $_ -Name "rule_id" }) +if ($reviewRules.Count -gt 0 -and $reviewRulesWithoutSamples.Count -eq 0) { + Add-Check -Key "review_rule_sample_coverage" -Label "审核规则样本覆盖" -Status "pass" -Detail "所有审核规则均绑定审核黄金样本" +} elseif ($reviewRules.Count -eq 0) { + Add-Check -Key "review_rule_sample_coverage" -Label "审核规则样本覆盖" -Status "blocked" -Detail "审核规则清单为空" +} else { + Add-Check -Key "review_rule_sample_coverage" -Label "审核规则样本覆盖" -Status "blocked" -Detail "以下审核规则缺少审核样本:$($reviewRulesWithoutSamples -join ',')" -Meta @{ rules = $reviewRulesWithoutSamples } +} + +$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]@{ + release_mode = $ReleaseMode + legal_sources = $legalSources.Count + knowledge_cards = $knowledgeCards.Count + review_rules = $reviewRules.Count + clauses = $clauses.Count + templates = $templates.Count + review_samples = $reviewSamples.Count + generation_samples = $generationSamples.Count + kb_qa_samples = $kbQaSamples.Count + release_candidates = $releaseCandidates.Count + release_ready_candidates = @($releaseCandidates | Where-Object { $_.release_ready }).Count + blocked_candidates = $blockedItems.Count + ready_legal_sources = $readyByType["legal_source"] + ready_knowledge_cards = $readyByType["knowledge_card"] + ready_review_rules = $readyByType["review_rule"] + ready_clauses = $readyByType["clause"] + ready_templates = $readyByType["contract_template"] +} + +$summary = [ordered]@{ + name = "phase13-production-pilot-release" + status = $overallStatus + generated_at = (Get-Date).ToUniversalTime().ToString("o") + branch = $branch + commit = $commit + remote = $remoteUrl + release_mode = $ReleaseMode + api_base_url = $ApiBaseUrl + allow_api_release = [bool]$AllowApiRelease + artifact_dir = $artifactDir + library_root = $LibraryRoot + checks = $checks.ToArray() + child_runs = @($childRuns | ForEach-Object { + [ordered]@{ + key = $_.key + label = $_.label + child_status = $_.child_status + step_status = $_.step_status + artifact_dir = $_.artifact_dir + summary_path = $_.summary_path + output_log = $_.output_log + exit_code = $_.exit_code + } + }) + metrics = $metrics + artifacts = [ordered]@{ + summary_json = $summaryJson + summary_md = $summaryMd + release_candidates_csv = $candidateCsv + blocked_items_csv = $blockedCsv + git_status = $gitStatusPath + } + totals = [ordered]@{ + total_checks = $checks.Count + failed_required = $failedRequired + blocked_required = $blockedRequired + warnings = $warnings + } +} +Save-Json -Path $summaryJson -Value $summary + +$overallStatusText = Convert-OverallStatusText -Status $overallStatus +$md = New-Object System.Collections.Generic.List[string] +$md.Add("# 阶段 13 生产试点发布闭环报告") | Out-Null +$md.Add("") | Out-Null +$md.Add("- 发布状态:$overallStatusText($overallStatus)") | Out-Null +$md.Add("- 发布模式:$ReleaseMode") | Out-Null +$md.Add("- 允许 API 发布:$([bool]$AllowApiRelease)") | Out-Null +$md.Add("- 生成时间:$($summary.generated_at)") | Out-Null +$md.Add("- 分支:$branch") | Out-Null +$md.Add("- 提交:$commit") | Out-Null +$md.Add("- API 地址:$ApiBaseUrl") | 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 +foreach ($run in $childRuns) { + $childArtifact = if ([string]::IsNullOrWhiteSpace($run.artifact_dir)) { "" } else { $run.artifact_dir.Replace("|", "/") } + $md.Add("| $($run.label) | $($run.child_status) | $childArtifact |") | 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.legal_sources) |") | Out-Null +$md.Add("| 知识卡 | $($metrics.knowledge_cards) |") | Out-Null +$md.Add("| 审核规则 | $($metrics.review_rules) |") | Out-Null +$md.Add("| 标准条款 | $($metrics.clauses) |") | Out-Null +$md.Add("| 合同模板 | $($metrics.templates) |") | Out-Null +$md.Add("| 审核样本 | $($metrics.review_samples) |") | Out-Null +$md.Add("| 生成样本 | $($metrics.generation_samples) |") | Out-Null +$md.Add("| KB 问答样本 | $($metrics.kb_qa_samples) |") | Out-Null +$md.Add("| 发布候选资产 | $($metrics.release_candidates) |") | Out-Null +$md.Add("| 可发布资产 | $($metrics.release_ready_candidates) |") | Out-Null +$md.Add("| 阻塞资产 | $($metrics.blocked_candidates) |") | Out-Null +$md.Add("| 可发布模板 | $($metrics.ready_templates) |") | 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 { "否" } + $statusText = Convert-CheckStatusText -Status $check.status + $detail = ([string]$check.detail).Replace("|", "/") + $md.Add("| $($check.label) | $requiredText | $statusText | $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-production-pilot-release-summary.json`') | Out-Null +$md.Add('- `phase13-production-pilot-release-summary.md`') | Out-Null +$md.Add('- `release-candidates.csv`') | Out-Null +$md.Add('- `blocked-release-items.csv`') | Out-Null +$md.Add('- `*-output.log`') | Out-Null +$md | Set-Content -Encoding UTF8 -Path $summaryMd + +Write-Host "PHASE13_PRODUCTION_PILOT_RELEASE_$overallStatus $artifactDir" +if ($overallStatus -eq "FAIL") { + throw "阶段 13 生产试点发布闭环未通过。查看:$summaryMd" +} +if ($overallStatus -eq "BLOCKED" -and $FailOnBlocked) { + throw "阶段 13 生产试点发布闭环被阻塞。查看:$summaryMd" +}