docs: add phase 13 pilot release gate

This commit is contained in:
Codex
2026-05-01 22:57:26 +08:00
parent 68d32668cd
commit f5cf88f7d2
5 changed files with 742 additions and 2 deletions
@@ -33,4 +33,5 @@
7. 阶段 13.5 黄金样本评测脚本已建立,可离线评测审核样本、生成样本和知识库问答样本。
8. 阶段 13.7 一键导入脚本已建立,默认 `Validate` 模式不改系统,`Review` 模式可导入待法务复核知识库和审核规则,`Production` 模式只导入已复核且生产就绪资料。
9. 阶段 13.7 入库闭环总控脚本已建立,可一键串联黄金样本评测、资料门禁和导入校验,并输出闭环证据包。
10. 生产发布仍需等待法务复核和业务复核完成。
10. 阶段 13.8 生产试点发布门禁脚本已建立,默认 `DryRun` 模式只做发布前验证,并在资料未复核或模板库缺失时输出阻塞证据。
11. 生产发布仍需等待法务复核、业务复核和合同模板库补齐完成。
@@ -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`,原因是无已复核可发布资产且合同模板库为空。
@@ -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`。
+6 -1
View File
@@ -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
@@ -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) + "(?<status>[A-Z_]+)\s+(?<artifact>.+?)\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"
}