433 lines
16 KiB
PowerShell
433 lines
16 KiB
PowerShell
param(
|
||
[ValidateSet("Validate", "Review", "Production")]
|
||
[string]$ImportMode = "Validate",
|
||
[string]$ApiBaseUrl = "http://127.0.0.1:8000/api/v1",
|
||
[string]$AdminEmail = "admin@example.com",
|
||
[string]$AdminPassword = "",
|
||
[string]$LibraryRoot = "",
|
||
[string]$ArtifactRoot = "",
|
||
[switch]$AllowApiImport,
|
||
[switch]$ForceReupload,
|
||
[switch]$SkipTemplates,
|
||
[switch]$SkipReviewRules,
|
||
[switch]$FailOnLegalReviewRequired
|
||
)
|
||
|
||
$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-ingestion-closed-loop"
|
||
}
|
||
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-ingestion-closed-loop-summary.json"
|
||
$summaryMd = Join-Path $artifactDir "phase13-ingestion-closed-loop-summary.md"
|
||
$gitStatusPath = Join-Path $artifactDir "git-status.txt"
|
||
|
||
$ApiBaseUrl = $ApiBaseUrl.TrimEnd("/")
|
||
$steps = New-Object System.Collections.Generic.List[object]
|
||
$childRuns = New-Object System.Collections.Generic.List[object]
|
||
|
||
function Add-Step {
|
||
param(
|
||
[string]$Key,
|
||
[string]$Label,
|
||
[ValidateSet("pass", "fail", "warn", "skip", "blocked")]
|
||
[string]$Status,
|
||
[string]$Detail,
|
||
[bool]$Required = $true,
|
||
[hashtable]$Meta = @{}
|
||
)
|
||
|
||
$steps.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 Convert-OverallStatusText {
|
||
param([string]$Status)
|
||
switch ($Status) {
|
||
"PASS" { return "通过" }
|
||
"WARN" { return "通过但有警告" }
|
||
"FAIL" { return "未通过" }
|
||
"BLOCKED" { return "阻塞" }
|
||
"PASS_WITH_LEGAL_REVIEW_REQUIRED" { return "通过但仍需法务复核" }
|
||
default { return $Status }
|
||
}
|
||
}
|
||
|
||
function Convert-StepStatusText {
|
||
param([string]$Status)
|
||
switch ($Status) {
|
||
"pass" { return "通过" }
|
||
"fail" { return "失败" }
|
||
"warn" { return "警告" }
|
||
"skip" { return "跳过" }
|
||
"blocked" { return "阻塞" }
|
||
default { return $Status }
|
||
}
|
||
}
|
||
|
||
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-Step -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 = ""
|
||
exit_code = 1
|
||
output_log = $logPath
|
||
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"
|
||
}
|
||
|
||
Add-Step -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
|
||
exit_code = $exitCode
|
||
output_log = $logPath
|
||
summary = $childSummary
|
||
}
|
||
$childRuns.Add($result) | Out-Null
|
||
return $result
|
||
}
|
||
|
||
if (-not (Test-Path "$HOME\.Codex\skills\gstack\bin")) {
|
||
Add-Step -Key "gstack" -Label "gstack 前置检查" -Status "fail" -Detail "gstack 缺失"
|
||
} else {
|
||
Add-Step -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-Step -Key "origin_remote" -Label "origin 远端仓库" -Status "pass" -Detail "origin 指向 192.168.3.90" -Meta @{ branch = $branch; commit = $commit; remote = $remoteUrl }
|
||
} else {
|
||
Add-Step -Key "origin_remote" -Label "origin 远端仓库" -Status "fail" -Detail "origin 未指向 192.168.3.90" -Meta @{ branch = $branch; commit = $commit; remote = $remoteUrl }
|
||
}
|
||
|
||
if ($gitStatus) {
|
||
Add-Step -Key "working_tree" -Label "工作区状态" -Status "warn" -Required $false -Detail "工作区存在未提交改动;按证据记录,不阻塞本地闭环" -Meta @{ artifact = $gitStatusPath }
|
||
} else {
|
||
Add-Step -Key "working_tree" -Label "工作区状态" -Status "pass" -Detail "工作区干净" -Meta @{ artifact = $gitStatusPath }
|
||
}
|
||
|
||
if (Test-Path -LiteralPath $LibraryRoot -PathType Container) {
|
||
Add-Step -Key "library_root" -Label "阶段 13 资料包根目录" -Status "pass" -Detail "资料包根目录存在" -Meta @{ root = $LibraryRoot }
|
||
} else {
|
||
Add-Step -Key "library_root" -Label "阶段 13 资料包根目录" -Status "fail" -Detail "资料包根目录缺失" -Meta @{ root = $LibraryRoot }
|
||
}
|
||
|
||
$commonArgs = @("-LibraryRoot", $LibraryRoot)
|
||
$reviewSensitiveArgs = @()
|
||
if ($FailOnLegalReviewRequired) {
|
||
$reviewSensitiveArgs += "-FailOnLegalReviewRequired"
|
||
}
|
||
|
||
$goldenRun = Invoke-StageScript `
|
||
-Key "golden_eval" `
|
||
-Label "黄金样本离线评测" `
|
||
-ScriptRelativePath "scripts\run-phase13-golden-eval.ps1" `
|
||
-Arguments ($commonArgs + $reviewSensitiveArgs) `
|
||
-ExpectedPrefix "PHASE13_GOLDEN_EVAL_" `
|
||
-SummaryFileName "phase13-golden-eval-summary.json"
|
||
|
||
$libraryGateRun = Invoke-StageScript `
|
||
-Key "library_gate" `
|
||
-Label "法律知识库资料门禁" `
|
||
-ScriptRelativePath "scripts\run-legal-knowledge-library-gate.ps1" `
|
||
-Arguments ($commonArgs + $reviewSensitiveArgs) `
|
||
-ExpectedPrefix "PHASE13_LEGAL_KNOWLEDGE_LIBRARY_GATE_" `
|
||
-SummaryFileName "legal-knowledge-library-gate-summary.json"
|
||
|
||
$importRun = $null
|
||
if ($ImportMode -ne "Validate" -and -not $AllowApiImport) {
|
||
Add-Step -Key "api_import_guard" -Label "API 写入保护" -Status "blocked" -Detail "非 Validate 模式必须显式传入 -AllowApiImport,避免未复核资料误写入系统" -Meta @{ import_mode = $ImportMode; api_base_url = $ApiBaseUrl }
|
||
} else {
|
||
$importArgs = @(
|
||
"-Mode", $ImportMode,
|
||
"-LibraryRoot", $LibraryRoot,
|
||
"-ApiBaseUrl", $ApiBaseUrl,
|
||
"-AdminEmail", $AdminEmail
|
||
)
|
||
if (-not [string]::IsNullOrWhiteSpace($AdminPassword)) {
|
||
$importArgs += @("-AdminPassword", $AdminPassword)
|
||
}
|
||
if ($ForceReupload) {
|
||
$importArgs += "-ForceReupload"
|
||
}
|
||
if ($SkipTemplates) {
|
||
$importArgs += "-SkipTemplates"
|
||
}
|
||
if ($SkipReviewRules) {
|
||
$importArgs += "-SkipReviewRules"
|
||
}
|
||
|
||
$importRun = Invoke-StageScript `
|
||
-Key "library_import" `
|
||
-Label "资料入库校验与导入" `
|
||
-ScriptRelativePath "scripts\import-phase13-library.ps1" `
|
||
-Arguments $importArgs `
|
||
-ExpectedPrefix "PHASE13_IMPORT_" `
|
||
-SummaryFileName "phase13-import-summary.json"
|
||
}
|
||
|
||
$failedRequired = @($steps | Where-Object { $_.required -and $_.status -eq "fail" }).Count
|
||
$blockedRequired = @($steps | Where-Object { $_.required -and $_.status -eq "blocked" }).Count
|
||
$warnings = @($steps | Where-Object { $_.status -eq "warn" }).Count
|
||
$legalReviewRequired = @($childRuns | Where-Object { $_.child_status -eq "PASS_WITH_LEGAL_REVIEW_REQUIRED" }).Count
|
||
|
||
$overallStatus = if ($failedRequired -gt 0) {
|
||
"FAIL"
|
||
} elseif ($blockedRequired -gt 0) {
|
||
"BLOCKED"
|
||
} elseif ($legalReviewRequired -gt 0) {
|
||
"PASS_WITH_LEGAL_REVIEW_REQUIRED"
|
||
} elseif ($warnings -gt 0) {
|
||
"WARN"
|
||
} else {
|
||
"PASS"
|
||
}
|
||
|
||
$metrics = [ordered]@{
|
||
import_mode = $ImportMode
|
||
golden_total_samples = if ($goldenRun.summary -and $goldenRun.summary.metrics) { $goldenRun.summary.metrics.total_samples } else { $null }
|
||
golden_pass_rate_percent = if ($goldenRun.summary -and $goldenRun.summary.metrics) { $goldenRun.summary.metrics.pass_rate_percent } else { $null }
|
||
legal_sources = if ($libraryGateRun.summary -and $libraryGateRun.summary.metrics) { $libraryGateRun.summary.metrics.legal_sources } else { $null }
|
||
knowledge_cards = if ($libraryGateRun.summary -and $libraryGateRun.summary.metrics) { $libraryGateRun.summary.metrics.knowledge_cards } else { $null }
|
||
review_rules = if ($libraryGateRun.summary -and $libraryGateRun.summary.metrics) { $libraryGateRun.summary.metrics.review_rules } else { $null }
|
||
clauses = if ($libraryGateRun.summary -and $libraryGateRun.summary.metrics) { $libraryGateRun.summary.metrics.clauses } else { $null }
|
||
templates = if ($importRun -and $importRun.summary -and $importRun.summary.totals) { $importRun.summary.totals.templates } else { $null }
|
||
import_validation_failures = if ($importRun -and $importRun.summary -and $importRun.summary.totals) { $importRun.summary.totals.validation_failures } else { $null }
|
||
import_failures = if ($importRun -and $importRun.summary -and $importRun.summary.totals) { $importRun.summary.totals.import_failures } else { $null }
|
||
}
|
||
|
||
$summary = [ordered]@{
|
||
name = "phase13-ingestion-closed-loop"
|
||
status = $overallStatus
|
||
generated_at = (Get-Date).ToUniversalTime().ToString("o")
|
||
branch = $branch
|
||
commit = $commit
|
||
remote = $remoteUrl
|
||
artifact_dir = $artifactDir
|
||
library_root = $LibraryRoot
|
||
import_mode = $ImportMode
|
||
api_base_url = $ApiBaseUrl
|
||
allow_api_import = [bool]$AllowApiImport
|
||
fail_on_legal_review_required = [bool]$FailOnLegalReviewRequired
|
||
steps = $steps.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
|
||
totals = [ordered]@{
|
||
total_steps = $steps.Count
|
||
failed_required = $failedRequired
|
||
blocked_required = $blockedRequired
|
||
warnings = $warnings
|
||
legal_review_required_children = $legalReviewRequired
|
||
}
|
||
}
|
||
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("- 生成时间:$($summary.generated_at)") | Out-Null
|
||
$md.Add("- 分支:$branch") | Out-Null
|
||
$md.Add("- 提交:$commit") | Out-Null
|
||
$md.Add("- 导入模式:$ImportMode") | Out-Null
|
||
$md.Add("- API 地址:$ApiBaseUrl") | Out-Null
|
||
$md.Add("- 允许 API 写入:$([bool]$AllowApiImport)") | 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) {
|
||
$artifact = if ([string]::IsNullOrWhiteSpace($run.artifact_dir)) { "" } else { $run.artifact_dir.Replace("|", "/") }
|
||
$md.Add("| $($run.label) | $($run.child_status) | $artifact |") | 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.golden_total_samples) |") | Out-Null
|
||
$md.Add("| 黄金样本通过率 | $($metrics.golden_pass_rate_percent)% |") | 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.import_validation_failures) |") | Out-Null
|
||
$md.Add("| 导入失败 | $($metrics.import_failures) |") | Out-Null
|
||
$md.Add("") | Out-Null
|
||
$md.Add("## 总控步骤") | Out-Null
|
||
$md.Add("") | Out-Null
|
||
$md.Add("| 步骤 | 必需 | 状态 | 说明 |") | Out-Null
|
||
$md.Add("|---|---:|---|---|") | Out-Null
|
||
foreach ($step in $steps) {
|
||
$requiredText = if ($step.required) { "是" } else { "否" }
|
||
$statusText = Convert-StepStatusText -Status $step.status
|
||
$detail = ([string]$step.detail).Replace("|", "/")
|
||
$md.Add("| $($step.label) | $requiredText | $statusText | $detail |") | Out-Null
|
||
}
|
||
$md.Add("") | Out-Null
|
||
$md.Add("## 当前结论") | Out-Null
|
||
$md.Add("") | Out-Null
|
||
if ($overallStatus -eq "PASS_WITH_LEGAL_REVIEW_REQUIRED") {
|
||
$md.Add("自动化闭环已经跑通,但资料仍处于法务复核状态;在完成复核前,不应作为生产确定性法律依据。") | Out-Null
|
||
} elseif ($overallStatus -eq "PASS") {
|
||
$md.Add("自动化闭环已通过,可进入下一步试点或生产发布前验证。") | Out-Null
|
||
} elseif ($overallStatus -eq "BLOCKED") {
|
||
$md.Add("闭环被保护条件阻塞。若需要真实 API 入库,请确认资料状态和环境后显式传入 `-AllowApiImport`。") | Out-Null
|
||
} else {
|
||
$md.Add("闭环未完全通过,请优先查看子门禁产物和输出日志。") | Out-Null
|
||
}
|
||
$md.Add("") | Out-Null
|
||
$md.Add("本地产物:") | Out-Null
|
||
$md.Add("") | Out-Null
|
||
$md.Add('- `phase13-ingestion-closed-loop-summary.json`') | Out-Null
|
||
$md.Add('- `phase13-ingestion-closed-loop-summary.md`') | Out-Null
|
||
$md.Add('- `*-output.log`') | Out-Null
|
||
$md | Set-Content -Encoding UTF8 -Path $summaryMd
|
||
|
||
Write-Host "PHASE13_INGESTION_CLOSED_LOOP_$overallStatus $artifactDir"
|
||
if ($overallStatus -eq "FAIL") {
|
||
throw "阶段 13 入库与自动化门禁闭环未通过。查看:$summaryMd"
|
||
}
|
||
if ($overallStatus -eq "BLOCKED") {
|
||
throw "阶段 13 入库与自动化门禁闭环被保护条件阻塞。查看:$summaryMd"
|
||
}
|
||
if ($overallStatus -eq "PASS_WITH_LEGAL_REVIEW_REQUIRED" -and $FailOnLegalReviewRequired) {
|
||
throw "阶段 13 入库与自动化门禁闭环仍需法务复核。查看:$summaryMd"
|
||
}
|