Files
ai-contract-platform/scripts/run-phase13-ingestion-closed-loop.ps1
T
2026-05-01 22:48:47 +08:00

433 lines
16 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("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"
}