Files
ai-contract-platform/scripts/run-phase13-production-pilot-release.ps1
T

655 lines
27 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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"
$reviewExecutionRun = Invoke-StageScript `
-Key "review_execution_gate" `
-Label "法务与业务复核执行门禁" `
-ScriptRelativePath "scripts\run-phase13-review-execution-gate.ps1" `
-Arguments @("-LibraryRoot", $LibraryRoot) `
-ExpectedPrefix "PHASE13_REVIEW_EXECUTION_GATE_" `
-SummaryFileName "phase13-review-execution-gate-summary.json"
$protectedTransitionRun = Invoke-StageScript `
-Key "protected_apply_transition_gate" `
-Label "受保护回填与资产状态迁移门禁" `
-ScriptRelativePath "scripts\run-phase13-protected-apply-transition-gate.ps1" `
-Arguments @("-LibraryRoot", $LibraryRoot) `
-ExpectedPrefix "PHASE13_PROTECTED_APPLY_TRANSITION_GATE_" `
-SummaryFileName "phase13-protected-apply-transition-gate-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"
}