655 lines
27 KiB
PowerShell
655 lines
27 KiB
PowerShell
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"
|
||
}
|