diff --git a/project-docs/phase13-legal-knowledge-template-library/evidence/phase13-review-return-preflight-workflow.md b/project-docs/phase13-legal-knowledge-template-library/evidence/phase13-review-return-preflight-workflow.md new file mode 100644 index 00000000..43bbac0c --- /dev/null +++ b/project-docs/phase13-legal-knowledge-template-library/evidence/phase13-review-return-preflight-workflow.md @@ -0,0 +1,42 @@ +# 阶段 13 回收结果预检与合并计划工作流 + +本文档说明真实复核结果回收后,如何在写入专家复核登记表前进行自动预检。 + +## 一、目标 + +1. 检查法务、业务和签收回收文件是否齐套。 +2. 校验字段、复核结果枚举、复核人、复核日期和证据路径。 +3. 与 `expert-review-register.csv` 做差异预览。 +4. 生成合并计划,先进入 DryRun,再允许受保护回填。 + +## 二、自动化脚本 + +```powershell +powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts\run-phase13-review-return-preflight.ps1 +``` + +脚本默认读取最新复核派发包中的: + +```text +reviewer-kits/return-templates/ +``` + +## 三、输出产物 + +1. `return-source-manifest.csv`:回收文件来源和字段结构。 +2. `return-validation-board.csv`:逐项预检看板。 +3. `return-preflight-blockers.csv`:阻塞项。 +4. `return-diff-preview.csv`:与专家复核登记表的差异预览。 +5. `return-merge-plan.csv`:回填前合并计划。 +6. `phase13-review-return-preflight-summary.json` 和 `.md`:门禁摘要。 + +## 四、门禁规则 + +1. 回收文件缺失或字段缺失为 `FAIL`。 +2. 回收文件仍为 `PENDING` 为 `BLOCKED`。 +3. 缺少复核人、复核日期或证据路径为 `BLOCKED`。 +4. 只有全部回收结果通过预检后,才允许进入 `apply-phase13-review-results.ps1` DryRun。 + +## 五、安全规则 + +本脚本只做预检和合并计划,不写入专家复核登记表,不修改资产状态,不推进生产发布。 diff --git a/project-docs/阶段13-法律知识库与合同模板库建设闭环第十八批执行记录-2026-05-02.md b/project-docs/阶段13-法律知识库与合同模板库建设闭环第十八批执行记录-2026-05-02.md new file mode 100644 index 00000000..351f9f70 --- /dev/null +++ b/project-docs/阶段13-法律知识库与合同模板库建设闭环第十八批执行记录-2026-05-02.md @@ -0,0 +1,66 @@ +# AI合同平台 阶段 13 法律知识库与合同模板库建设闭环第十八批执行记录(2026-05-02) + +## 一、阶段结论 + +本批次完成 `回收结果预检与差异合并工作台`,当前状态为 `BLOCKED_ON_REAL_RETURN_INPUT`。 + +第十七批已经能导出复核派发包、回收模板和哈希留痕。本批次继续补齐回收后的自动化预检能力,确保真实复核结果在写入专家复核登记表前先完成字段校验、枚举校验、证据路径校验、差异预览和合并计划。 + +## 二、本批次交付 + +新增或更新内容: + +1. `scripts/run-phase13-review-return-preflight.ps1`。 +2. `scripts/run-phase13-review-control-center.ps1`,把回收预检纳入总控。 +3. `scripts/run-legal-knowledge-library-gate.ps1`,纳入新增脚本检查。 +4. `project-docs/phase13-legal-knowledge-template-library/evidence/phase13-review-return-preflight-workflow.md`。 + +## 三、回收预检范围 + +脚本默认读取最新复核派发包中的 `reviewer-kits/return-templates/`,覆盖: + +| 回收文件 | 数量 | +|---|---:| +| 法务复核回收记录 | 22 | +| 业务复核回收记录 | 18 | +| 签收回收记录 | 2 | +| 预检总记录 | 42 | + +## 四、自动化门禁 + +门禁规则: + +1. 回收文件缺失或字段缺失为 `FAIL`。 +2. 回收结果仍为 `PENDING` 为 `BLOCKED`。 +3. 缺少复核人、复核日期或证据路径为 `BLOCKED`。 +4. 只有通过预检后,才允许进入 `apply-phase13-review-results.ps1` DryRun。 + +## 五、当前状态 + +当前回收模板仍是派发前模板,所有复核结论均保持 `PENDING`,签收也保持未完成。因此门禁保持 `BLOCKED` 是正确状态。 + +## 六、自动化验证结果 + +```text +PHASE13_REVIEW_RETURN_PREFLIGHT_BLOCKED +PHASE13_LEGAL_KNOWLEDGE_LIBRARY_GATE_PASS_WITH_LEGAL_REVIEW_REQUIRED +PHASE13_REVIEW_CONTROL_CENTER_BLOCKED +``` + +关键指标: + +| 指标 | 结果 | +|---|---:| +| 法务回收记录 | 22 | +| 业务回收记录 | 18 | +| 签收回收记录 | 2 | +| 预检记录 | 42 | +| 可进入 DryRun 回填 | 0 | +| 回收预检阻塞项 | 42 | +| 总控准入评分 | 3/13 | + +## 七、下一步 + +下一步可以继续自动推进“真实回填保护与资产状态迁移预案”,把预检通过后的合并计划进一步转成受保护写入、资产状态迁移和回滚证据。 + +阶段 13 第十八批当前状态:`BLOCKED_ON_REAL_RETURN_INPUT`。 diff --git a/scripts/run-legal-knowledge-library-gate.ps1 b/scripts/run-legal-knowledge-library-gate.ps1 index 6ff1a122..a9b6267f 100644 --- a/scripts/run-legal-knowledge-library-gate.ps1 +++ b/scripts/run-legal-knowledge-library-gate.ps1 @@ -321,6 +321,10 @@ function Test-SecretLeak { if (Test-Path -LiteralPath $reviewerKitExportScriptPath -PathType Leaf) { $files += Get-Item -LiteralPath $reviewerKitExportScriptPath } + $reviewReturnPreflightScriptPath = Join-Path $repoRoot "scripts\run-phase13-review-return-preflight.ps1" + if (Test-Path -LiteralPath $reviewReturnPreflightScriptPath -PathType Leaf) { + $files += Get-Item -LiteralPath $reviewReturnPreflightScriptPath + } foreach ($file in $files) { $match = Select-String -LiteralPath $file.FullName -Pattern $adminPasswordLiteral -List -ErrorAction SilentlyContinue if ($match) { @@ -402,6 +406,7 @@ $commonDocFiles = @( "scripts\run-phase13-review-task-package-gate.ps1", "scripts\run-phase13-review-intake-gate.ps1", "scripts\export-phase13-reviewer-kit.ps1", + "scripts\run-phase13-review-return-preflight.ps1", "project-docs\phase13-legal-knowledge-template-library\review-intake\reviewer-intake-template.csv", "project-docs\phase13-legal-knowledge-template-library\review-intake\README.md" ) diff --git a/scripts/run-phase13-review-control-center.ps1 b/scripts/run-phase13-review-control-center.ps1 index af61ff4d..a9f69d77 100644 --- a/scripts/run-phase13-review-control-center.ps1 +++ b/scripts/run-phase13-review-control-center.ps1 @@ -213,6 +213,7 @@ $stages = @( [pscustomobject]@{ key = "review_intake"; label = "真实复核输入接入"; root = "project-docs\qa-artifacts\phase13-review-intake-gate"; summary = "phase13-review-intake-gate-summary.json" }, [pscustomobject]@{ key = "task_package"; label = "复核任务包与验收追踪"; root = "project-docs\qa-artifacts\phase13-review-task-package-gate"; summary = "phase13-review-task-package-gate-summary.json" }, [pscustomobject]@{ key = "reviewer_kit"; label = "复核派发包导出"; root = "project-docs\qa-artifacts\phase13-reviewer-kit-export"; summary = "phase13-reviewer-kit-export-summary.json" }, + [pscustomobject]@{ key = "return_preflight"; label = "回收结果预检与合并计划"; root = "project-docs\qa-artifacts\phase13-review-return-preflight"; summary = "phase13-review-return-preflight-summary.json" }, [pscustomobject]@{ key = "handoff"; label = "复核交付回收"; root = "project-docs\qa-artifacts\phase13-review-handoff-gate"; summary = "phase13-review-handoff-gate-summary.json" }, [pscustomobject]@{ key = "progress"; label = "复核进度催办"; root = "project-docs\qa-artifacts\phase13-review-progress-gate"; summary = "phase13-review-progress-gate-summary.json" }, [pscustomobject]@{ key = "result_apply"; label = "复核结果回填"; root = "project-docs\qa-artifacts\phase13-review-result-apply"; summary = "phase13-review-result-apply-summary.json" }, @@ -242,6 +243,7 @@ $firstWave = $summaries["first_wave"] $reviewIntake = $summaries["review_intake"] $taskPackage = $summaries["task_package"] $reviewerKit = $summaries["reviewer_kit"] +$returnPreflight = $summaries["return_preflight"] $handoff = $summaries["handoff"] $progress = $summaries["progress"] $resultApply = $summaries["result_apply"] @@ -263,6 +265,9 @@ $taskPackageBlockedOrders = [int](Get-MetricValue -Summary $taskPackage -Name "b $reviewerKitFiles = [int](Get-MetricValue -Summary $reviewerKit -Name "kit_files") $reviewerKitHashRows = [int](Get-MetricValue -Summary $reviewerKit -Name "hash_manifest_rows") $reviewerKitReadyOrders = [int](Get-MetricValue -Summary $reviewerKit -Name "task_ready_to_dispatch_orders") +$returnPreflightRows = [int](Get-MetricValue -Summary $returnPreflight -Name "validation_rows") +$returnPreflightMergeReadyRows = [int](Get-MetricValue -Summary $returnPreflight -Name "merge_ready_rows") +$returnPreflightBlockers = [int](Get-MetricValue -Summary $returnPreflight -Name "blocker_count") $handoffBlockers = [int](Get-MetricValue -Summary $handoff -Name "blockers") $handoffAckRows = [int](Get-MetricValue -Summary $handoff -Name "acknowledged_rows") $ackPending = [int](Get-MetricValue -Summary $progress -Name "ack_pending") @@ -281,6 +286,7 @@ Add-Score -Key "business_reviewer_assigned" -Label "真实业务复核人已分 Add-Score -Key "review_intake_ready" -Label "真实复核输入配置已齐套" -Ready ($reviewIntakeStatus -eq "PASS" -and $intakeRequiredRoles -gt 0 -and $intakeReadyRoles -eq $intakeRequiredRoles) -Evidence "intake_status=$reviewIntakeStatus; ready_roles=$intakeReadyRoles; required_roles=$intakeRequiredRoles; blocked_roles=$intakeBlockedRoles" -Blocker "真实复核输入配置未齐套" Add-Score -Key "review_task_package_generated" -Label "复核任务包与验收追踪已生成" -Ready ($taskPackageAcceptanceRows -ge 42) -Evidence "acceptance_tracking_rows=$taskPackageAcceptanceRows; ready_to_dispatch_orders=$taskPackageReadyOrders; blocked_orders=$taskPackageBlockedOrders" -Blocker "复核任务包或验收追踪未齐套" Add-Score -Key "reviewer_kit_exported" -Label "复核派发包已导出并哈希留痕" -Ready ($reviewerKitFiles -gt 0 -and $reviewerKitHashRows -gt 0) -Evidence "kit_files=$reviewerKitFiles; hash_manifest_rows=$reviewerKitHashRows; ready_orders=$reviewerKitReadyOrders" -Blocker "复核派发包尚未导出" +Add-Score -Key "return_preflight_generated" -Label "回收结果预检与合并计划已生成" -Ready ($returnPreflightRows -ge 42) -Evidence "validation_rows=$returnPreflightRows; merge_ready_rows=$returnPreflightMergeReadyRows; blockers=$returnPreflightBlockers" -Blocker "回收结果预检或合并计划未生成" Add-Score -Key "review_due_date_set" -Label "复核截止日期已设置" -Ready (-not $firstWaveDueMissing) -Evidence "due_date_missing=$firstWaveDueMissing" -Blocker "缺少复核截止日期" Add-Score -Key "handoff_acknowledged" -Label "复核交付已签收" -Ready ($ackPending -eq 0 -and $handoffAckRows -gt 0) -Evidence "ack_pending=$ackPending; acknowledged_rows=$handoffAckRows" -Blocker "签收未完成" Add-Score -Key "review_activity_started" -Label "真实复核活动已发生" -Ready ($completedReviews -gt 0 -or $pendingReviews -lt 22) -Evidence "pending_reviews=$pendingReviews; completed_reviews=$completedReviews" -Blocker "没有真实复核活动记录" @@ -310,6 +316,9 @@ if ($taskPackageAcceptanceRows -lt 42) { if ($reviewerKitFiles -eq 0 -or $reviewerKitHashRows -eq 0) { Add-NextAction -Priority "P0" -Owner "法律资料运营" -Action "导出复核派发包并生成哈希留痕" -Acceptance "kit_files>0 且 hash_manifest_rows>0" -Gate "export-phase13-reviewer-kit.ps1" } +if ($returnPreflightRows -lt 42) { + Add-NextAction -Priority "P0" -Owner "法律资料运营" -Action "生成回收结果预检与合并计划" -Acceptance "validation_rows>=42" -Gate "run-phase13-review-return-preflight.ps1" +} if ($firstWaveDueMissing) { Add-NextAction -Priority "P0" -Owner "交付负责人" -Action "设置首批 P0 复核截止日期" -Acceptance "due_date_missing=false" -Gate "run-phase13-first-wave-review-readiness.ps1 / run-phase13-review-progress-gate.ps1" } @@ -382,6 +391,9 @@ $metrics = [ordered]@{ reviewer_kit_files = $reviewerKitFiles reviewer_kit_hash_rows = $reviewerKitHashRows reviewer_kit_ready_orders = $reviewerKitReadyOrders + return_preflight_rows = $returnPreflightRows + return_preflight_merge_ready_rows = $returnPreflightMergeReadyRows + return_preflight_blockers = $returnPreflightBlockers missing_legal_reviewer_tasks = $missingLegalReviewer missing_business_reviewer_tasks = $missingBusinessReviewer ack_pending = $ackPending @@ -447,6 +459,9 @@ $md.Add("| 任务包可派发项 | $($metrics.task_package_ready_orders) |") | O $md.Add("| 任务包阻塞项 | $($metrics.task_package_blocked_orders) |") | Out-Null $md.Add("| 派发包文件 | $($metrics.reviewer_kit_files) |") | Out-Null $md.Add("| 派发包哈希留痕 | $($metrics.reviewer_kit_hash_rows) |") | Out-Null +$md.Add("| 回收预检记录 | $($metrics.return_preflight_rows) |") | Out-Null +$md.Add("| 回收预检可合并项 | $($metrics.return_preflight_merge_ready_rows) |") | Out-Null +$md.Add("| 回收预检阻塞项 | $($metrics.return_preflight_blockers) |") | Out-Null $md.Add("| 未分配法务复核 | $($metrics.missing_legal_reviewer_tasks) |") | Out-Null $md.Add("| 未分配业务复核 | $($metrics.missing_business_reviewer_tasks) |") | Out-Null $md.Add("| 未完成签收 | $($metrics.ack_pending) |") | Out-Null diff --git a/scripts/run-phase13-review-return-preflight.ps1 b/scripts/run-phase13-review-return-preflight.ps1 new file mode 100644 index 00000000..45ed706c --- /dev/null +++ b/scripts/run-phase13-review-return-preflight.ps1 @@ -0,0 +1,472 @@ +param( + [string]$ReturnRoot = "", + [string]$ReviewRegisterPath = "", + [string]$ArtifactRoot = "", + [switch]$FailOnBlocked +) + +$ErrorActionPreference = "Stop" + +$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path +$timestamp = Get-Date -Format "yyyy-MM-ddTHH-mm-ss-fff" + +function Resolve-LatestArtifactDir { + param([string]$RootRelativePath) + $root = Join-Path $repoRoot $RootRelativePath + $latest = Get-ChildItem -LiteralPath $root -Directory -ErrorAction SilentlyContinue | + Sort-Object Name -Descending | + Select-Object -First 1 + if ($latest) { + return $latest.FullName + } + return "" +} + +if ([string]::IsNullOrWhiteSpace($ReturnRoot)) { + $latestKit = Resolve-LatestArtifactDir -RootRelativePath "project-docs\qa-artifacts\phase13-reviewer-kit-export" + if (-not [string]::IsNullOrWhiteSpace($latestKit)) { + $ReturnRoot = Join-Path $latestKit "reviewer-kits\return-templates" + } +} +if (-not [string]::IsNullOrWhiteSpace($ReturnRoot) -and -not [System.IO.Path]::IsPathRooted($ReturnRoot)) { + $ReturnRoot = Join-Path $repoRoot $ReturnRoot +} + +if ([string]::IsNullOrWhiteSpace($ReviewRegisterPath)) { + $ReviewRegisterPath = Join-Path $repoRoot "project-docs\phase13-legal-knowledge-template-library\inventories\expert-review-register.csv" +} +if (-not [System.IO.Path]::IsPathRooted($ReviewRegisterPath)) { + $ReviewRegisterPath = Join-Path $repoRoot $ReviewRegisterPath +} + +if ([string]::IsNullOrWhiteSpace($ArtifactRoot)) { + $ArtifactRoot = Join-Path $repoRoot "project-docs\qa-artifacts\phase13-review-return-preflight" +} +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-review-return-preflight-summary.json" +$summaryMd = Join-Path $artifactDir "phase13-review-return-preflight-summary.md" +$sourceManifestCsv = Join-Path $artifactDir "return-source-manifest.csv" +$validationBoardCsv = Join-Path $artifactDir "return-validation-board.csv" +$blockersCsv = Join-Path $artifactDir "return-preflight-blockers.csv" +$diffPreviewCsv = Join-Path $artifactDir "return-diff-preview.csv" +$mergePlanCsv = Join-Path $artifactDir "return-merge-plan.csv" +$gitStatusPath = Join-Path $artifactDir "git-status.txt" + +$checks = New-Object System.Collections.Generic.List[object] +$sourceRows = New-Object System.Collections.Generic.List[object] +$validationRows = New-Object System.Collections.Generic.List[object] +$blockerRows = New-Object System.Collections.Generic.List[object] +$diffRows = New-Object System.Collections.Generic.List[object] +$mergeRows = New-Object System.Collections.Generic.List[object] + +$reviewRequiredColumns = @("review_id", "asset_type", "asset_id", "asset_name", "reviewer_role", "reviewer", "review_date", "review_result", "required_changes", "evidence_path", "allow_review_import", "allow_production_release", "notes") +$ackRequiredColumns = @("work_order_id", "recipient_role", "recipient", "handoff_owner", "acknowledged", "ack_date", "package_path", "missing_materials", "notes") +$allowedReviewResults = @("PENDING", "CHANGES_REQUIRED", "APPROVED_FOR_REVIEW_IMPORT", "APPROVED_FOR_PRODUCTION", "REJECTED") + +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 Add-Blocker { + param([string]$Kind, [string]$Id, [string]$Severity, [string]$Blocker, [string]$Evidence) + $blockerRows.Add([pscustomobject][ordered]@{ + kind = $Kind + id = $Id + severity = $Severity + blocker = $Blocker + evidence = $Evidence + }) | Out-Null +} + +function Save-Json { + param([string]$Path, [object]$Value) + $Value | ConvertTo-Json -Depth 50 | 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 Get-CsvHeader { + param([string]$Path) + if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { + return @() + } + $firstLine = Get-Content -LiteralPath $Path -Encoding UTF8 -TotalCount 1 + if ([string]::IsNullOrWhiteSpace($firstLine)) { + return @() + } + return @($firstLine.Split(",") | ForEach-Object { $_.Trim().Trim('"') }) +} + +function Test-RequiredColumns { + param([string]$Path, [string[]]$Columns) + $header = Get-CsvHeader -Path $Path + return @($Columns | Where-Object { $header -notcontains $_ }) +} + +function Test-Truthy { + param([string]$Value) + if ([string]::IsNullOrWhiteSpace($Value)) { + return $false + } + return @("true", "yes", "1", "是", "已签收") -contains $Value.Trim().ToLowerInvariant() +} + +function Test-PendingName { + param([string]$Value) + if ([string]::IsNullOrWhiteSpace($Value)) { + return $true + } + return ($Value -match "待指定|待确认|待复核|占位|示例") +} + +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 } + } +} + +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 $ReturnRoot -PathType Container) { + Add-Check -Key "return_root" -Label "回收文件目录" -Status "pass" -Detail "回收文件目录存在" -Meta @{ path = $ReturnRoot } +} else { + Add-Check -Key "return_root" -Label "回收文件目录" -Status "fail" -Detail "回收文件目录不存在" -Meta @{ path = $ReturnRoot } +} + +$registerRows = Import-CsvRows -Path $ReviewRegisterPath +$registerByReviewId = @{} +foreach ($registerRow in $registerRows) { + if ($registerRow.review_id) { + $registerByReviewId[$registerRow.review_id] = $registerRow + } +} + +$returnFiles = @( + [pscustomobject]@{ kind = "legal"; path = Join-Path $ReturnRoot "legal-review-result-return.csv"; required_columns = $reviewRequiredColumns }, + [pscustomobject]@{ kind = "business"; path = Join-Path $ReturnRoot "business-review-result-return.csv"; required_columns = $reviewRequiredColumns }, + [pscustomobject]@{ kind = "ack"; path = Join-Path $ReturnRoot "handoff-ack-return.csv"; required_columns = $ackRequiredColumns } +) + +$allReviewReturns = New-Object System.Collections.Generic.List[object] +$ackReturns = @() +foreach ($returnFile in $returnFiles) { + $exists = Test-Path -LiteralPath $returnFile.path -PathType Leaf + $missing = if ($exists) { Test-RequiredColumns -Path $returnFile.path -Columns $returnFile.required_columns } else { $returnFile.required_columns } + $rows = if ($exists) { Import-CsvRows -Path $returnFile.path } else { @() } + $sourceRows.Add([pscustomobject][ordered]@{ + kind = $returnFile.kind + path = $returnFile.path + exists = $exists + rows = $rows.Count + missing_columns = ($missing -join ";") + status = if ($exists -and $missing.Count -eq 0) { "ready" } else { "invalid" } + }) | Out-Null + if (-not $exists) { + Add-Blocker -Kind $returnFile.kind -Id $returnFile.path -Severity "P0" -Blocker "回收文件缺失" -Evidence $returnFile.path + } elseif ($missing.Count -gt 0) { + Add-Blocker -Kind $returnFile.kind -Id $returnFile.path -Severity "P0" -Blocker "回收文件字段缺失" -Evidence ($missing -join ",") + } + if ($returnFile.kind -eq "ack") { + $ackReturns = $rows + } else { + foreach ($row in $rows) { + $allReviewReturns.Add($row) | Out-Null + } + } +} +$sourceRows | Export-Csv -LiteralPath $sourceManifestCsv -NoTypeInformation -Encoding UTF8 + +foreach ($row in $allReviewReturns) { + $reviewResult = if ($row.review_result) { $row.review_result.Trim() } else { "" } + $reviewId = [string]$row.review_id + $inRegister = $registerByReviewId.ContainsKey($reviewId) + $resultAllowed = $allowedReviewResults -contains $reviewResult + $reviewerReady = -not (Test-PendingName -Value $row.reviewer) + $dateReady = -not [string]::IsNullOrWhiteSpace($row.review_date) + $evidenceReady = -not [string]::IsNullOrWhiteSpace($row.evidence_path) + $completed = ($reviewResult -ne "PENDING" -and $resultAllowed -and $reviewerReady -and $dateReady -and $evidenceReady) + $mergeAction = if ($completed) { "READY_FOR_DRY_RUN_APPLY" } else { "HOLD" } + + if (-not $inRegister) { + Add-Blocker -Kind "review_return" -Id $reviewId -Severity "P0" -Blocker "复核编号不在专家复核登记表中" -Evidence $reviewId + } + if (-not $resultAllowed) { + Add-Blocker -Kind "review_return" -Id $reviewId -Severity "P0" -Blocker "复核结果枚举非法" -Evidence $reviewResult + } + if ($reviewResult -eq "PENDING") { + Add-Blocker -Kind "review_return" -Id $reviewId -Severity "P0" -Blocker "复核结果仍为 PENDING" -Evidence $reviewId + } elseif (-not $completed) { + Add-Blocker -Kind "review_return" -Id $reviewId -Severity "P0" -Blocker "复核结果缺少复核人、日期或证据路径" -Evidence "reviewer=$($row.reviewer); date=$($row.review_date); evidence=$($row.evidence_path)" + } + + $validationRows.Add([pscustomobject][ordered]@{ + kind = "review_return" + review_id = $reviewId + asset_id = $row.asset_id + reviewer_role = $row.reviewer_role + reviewer = $row.reviewer + review_result = $reviewResult + in_register = $inRegister + result_allowed = $resultAllowed + reviewer_ready = $reviewerReady + date_ready = $dateReady + evidence_ready = $evidenceReady + completed = $completed + merge_action = $mergeAction + }) | Out-Null + + $current = if ($inRegister) { $registerByReviewId[$reviewId] } else { $null } + $diffRows.Add([pscustomobject][ordered]@{ + review_id = $reviewId + field = "review_result" + current_value = if ($current) { $current.review_result } else { "" } + returned_value = $reviewResult + merge_action = $mergeAction + }) | Out-Null + + $mergeRows.Add([pscustomobject][ordered]@{ + review_id = $reviewId + asset_type = $row.asset_type + asset_id = $row.asset_id + merge_action = $mergeAction + next_gate = "apply-phase13-review-results.ps1" + notes = if ($completed) { "可进入 DryRun 回填校验" } else { "等待真实复核结果齐套" } + }) | Out-Null +} + +foreach ($ack in $ackReturns) { + $acknowledged = Test-Truthy -Value $ack.acknowledged + $dateReady = -not [string]::IsNullOrWhiteSpace($ack.ack_date) + $recipientReady = -not (Test-PendingName -Value $ack.recipient) + $completed = ($acknowledged -and $dateReady -and $recipientReady) + if (-not $completed) { + Add-Blocker -Kind "ack_return" -Id $ack.work_order_id -Severity "P0" -Blocker "签收回收未完成" -Evidence "acknowledged=$($ack.acknowledged); ack_date=$($ack.ack_date); recipient=$($ack.recipient)" + } + $validationRows.Add([pscustomobject][ordered]@{ + kind = "ack_return" + review_id = $ack.work_order_id + asset_id = "" + reviewer_role = $ack.recipient_role + reviewer = $ack.recipient + review_result = if ($acknowledged) { "ACKNOWLEDGED" } else { "PENDING" } + in_register = $true + result_allowed = $true + reviewer_ready = $recipientReady + date_ready = $dateReady + evidence_ready = $true + completed = $completed + merge_action = if ($completed) { "READY_FOR_PROGRESS_GATE" } else { "HOLD" } + }) | Out-Null +} + +$validationRows | Export-Csv -LiteralPath $validationBoardCsv -NoTypeInformation -Encoding UTF8 +$blockerRows | Export-Csv -LiteralPath $blockersCsv -NoTypeInformation -Encoding UTF8 +$diffRows | Export-Csv -LiteralPath $diffPreviewCsv -NoTypeInformation -Encoding UTF8 +$mergeRows | Export-Csv -LiteralPath $mergePlanCsv -NoTypeInformation -Encoding UTF8 + +$completedReviewReturns = @($validationRows | Where-Object { $_.kind -eq "review_return" -and $_.completed }).Count +$pendingReviewReturns = @($validationRows | Where-Object { $_.kind -eq "review_return" -and -not $_.completed }).Count +$completedAckReturns = @($validationRows | Where-Object { $_.kind -eq "ack_return" -and $_.completed }).Count +$pendingAckReturns = @($validationRows | Where-Object { $_.kind -eq "ack_return" -and -not $_.completed }).Count +$mergeReadyRows = @($mergeRows | Where-Object { $_.merge_action -eq "READY_FOR_DRY_RUN_APPLY" }).Count + +if ($sourceRows.Count -eq 3 -and @($sourceRows | Where-Object { $_.status -eq "ready" }).Count -eq 3) { + Add-Check -Key "return_files" -Label "回收文件结构" -Status "pass" -Detail "回收文件结构齐套" +} else { + Add-Check -Key "return_files" -Label "回收文件结构" -Status "fail" -Detail "回收文件结构缺失或字段不完整" +} + +if ($validationRows.Count -gt 0) { + Add-Check -Key "validation_board" -Label "回收预检看板" -Status "pass" -Detail "已生成 $($validationRows.Count) 条回收预检记录" +} else { + Add-Check -Key "validation_board" -Label "回收预检看板" -Status "fail" -Detail "未生成回收预检记录" +} + +if ($completedReviewReturns -gt 0 -and $pendingReviewReturns -eq 0 -and $pendingAckReturns -eq 0) { + Add-Check -Key "return_completion" -Label "真实回收结果齐套" -Status "pass" -Detail "复核与签收回收结果均已齐套" +} else { + Add-Check -Key "return_completion" -Label "真实回收结果齐套" -Status "blocked" -Detail "completed_reviews=$completedReviewReturns; pending_reviews=$pendingReviewReturns; completed_ack=$completedAckReturns; pending_ack=$pendingAckReturns" +} + +if ($blockerRows.Count -eq 0) { + Add-Check -Key "return_blockers" -Label "回收预检阻塞项" -Status "pass" -Detail "无回收预检阻塞项" +} else { + Add-Check -Key "return_blockers" -Label "回收预检阻塞项" -Status "blocked" -Detail "回收预检阻塞项=$($blockerRows.Count)" +} + +$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]@{ + legal_return_rows = @($validationRows | Where-Object { $_.kind -eq "review_return" -and $_.reviewer_role -eq "法务" }).Count + business_return_rows = @($validationRows | Where-Object { $_.kind -eq "review_return" -and $_.reviewer_role -eq "业务" }).Count + ack_return_rows = @($validationRows | Where-Object { $_.kind -eq "ack_return" }).Count + validation_rows = $validationRows.Count + completed_review_returns = $completedReviewReturns + pending_review_returns = $pendingReviewReturns + completed_ack_returns = $completedAckReturns + pending_ack_returns = $pendingAckReturns + merge_ready_rows = $mergeReadyRows + blocker_count = $blockerRows.Count +} + +$summary = [ordered]@{ + name = "phase13-review-return-preflight" + status = $overallStatus + generated_at = (Get-Date).ToUniversalTime().ToString("o") + branch = $branch + commit = $commit + remote = $remoteUrl + return_root = $ReturnRoot + review_register_path = $ReviewRegisterPath + checks = $checks.ToArray() + metrics = $metrics + artifacts = [ordered]@{ + summary_json = $summaryJson + summary_md = $summaryMd + source_manifest = $sourceManifestCsv + validation_board = $validationBoardCsv + blockers = $blockersCsv + diff_preview = $diffPreviewCsv + merge_plan = $mergePlanCsv + git_status = $gitStatusPath + } + totals = [ordered]@{ + total_checks = $checks.Count + failed_required = $failedRequired + blocked_required = $blockedRequired + warnings = $warnings + } +} +Save-Json -Path $summaryJson -Value $summary + +$md = New-Object System.Collections.Generic.List[string] +$md.Add("# 阶段 13 回收结果预检与合并计划门禁") | Out-Null +$md.Add("") | Out-Null +$md.Add("- 门禁状态:$(Convert-OverallStatusText -Status $overallStatus)($overallStatus)") | Out-Null +$md.Add("- 生成时间:$($summary.generated_at)") | Out-Null +$md.Add("- 分支:$branch") | Out-Null +$md.Add("- 提交:$commit") | Out-Null +$md.Add("- 回收目录:$ReturnRoot") | 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 +$md.Add("| 法务回收记录 | $($metrics.legal_return_rows) |") | Out-Null +$md.Add("| 业务回收记录 | $($metrics.business_return_rows) |") | Out-Null +$md.Add("| 签收回收记录 | $($metrics.ack_return_rows) |") | Out-Null +$md.Add("| 预检记录 | $($metrics.validation_rows) |") | Out-Null +$md.Add("| 已完成复核回收 | $($metrics.completed_review_returns) |") | Out-Null +$md.Add("| 待完成复核回收 | $($metrics.pending_review_returns) |") | Out-Null +$md.Add("| 待完成签收回收 | $($metrics.pending_ack_returns) |") | Out-Null +$md.Add("| 可进入 DryRun 回填 | $($metrics.merge_ready_rows) |") | Out-Null +$md.Add("| 阻塞项 | $($metrics.blocker_count) |") | 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 { "否" } + $md.Add("| $($check.label) | $requiredText | $(Convert-CheckStatusText -Status $check.status) | $($check.detail) |") | Out-Null +} +$md.Add("") | Out-Null +$md.Add("## 当前结论") | Out-Null +$md.Add("") | Out-Null +if ($overallStatus -eq "BLOCKED") { + $md.Add("回收预检和合并计划已生成,但当前回收模板仍为 PENDING,不能进入真实回填。") | Out-Null +} elseif ($overallStatus -eq "PASS") { + $md.Add("回收结果已通过预检,可进入 DryRun 回填校验。") | Out-Null +} else { + $md.Add("回收预检未完全通过,请查看阻塞项和合并计划。") | Out-Null +} +$md | Set-Content -Encoding UTF8 -Path $summaryMd + +Write-Host "PHASE13_REVIEW_RETURN_PREFLIGHT_$overallStatus $artifactDir" +if ($overallStatus -eq "FAIL") { + throw "阶段 13 回收结果预检门禁未通过。查看:$summaryMd" +} +if ($overallStatus -eq "BLOCKED" -and $FailOnBlocked) { + throw "阶段 13 回收结果预检门禁被阻塞。查看:$summaryMd" +}