docs: add phase 13 pilot release gate
This commit is contained in:
@@ -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"
|
||||
}
|
||||
Reference in New Issue
Block a user