From 61cb3daf68f31edc04c6a9907addeab8a8372852 Mon Sep 17 00:00:00 2001 From: yuqei Date: Wed, 13 May 2026 12:10:52 +0800 Subject: [PATCH] feat: add reviewer assignment matrix gate --- ...µ·草与审查助手-当前工作进度保存-2026-05-11.md | 36 ++ .../collaboration-packages/README.md | 17 +- ...rmal-launch-reviewer-assignment-matrix.ps1 | 381 ++++++++++++++++++ 3 files changed, 427 insertions(+), 7 deletions(-) create mode 100644 scripts/apply-formal-launch-reviewer-assignment-matrix.ps1 diff --git a/project-docs/AI合同起草与审查助手-当前工作进度保存-2026-05-11.md b/project-docs/AI合同起草与审查助手-当前工作进度保存-2026-05-11.md index e223660c..8cf712a0 100644 --- a/project-docs/AI合同起草与审查助手-当前工作进度保存-2026-05-11.md +++ b/project-docs/AI合同起草与审查助手-当前工作进度保存-2026-05-11.md @@ -715,3 +715,39 @@ - 真实 reviewer 完成审查、签收、证据留存。 - 最终上线批准人签署 `FINAL_APPROVER/final-launch-approval-checklist.md`。 - 本轮只导入正式治理台账和修正统计口径,没有伪造任何批准结论。 + +## 2026-05-13 真实 reviewer 分配矩阵入口补强 + +### 本轮完成项 +- 新增 `scripts/apply-formal-launch-reviewer-assignment-matrix.ps1`: + - 默认只校验 `reviewer-assignment-matrix.csv`,不修改台账。 + - 真实法务、业务、运营、最终批准人四组仍为 `TBD`、`PENDING`、`待复核` 等占位时输出 `BLOCKED`。 + - 四组均填写真实姓名后输出 `PASS`。 + - 只有显式加 `-Apply` 时,才会把真实分配写回复核任务包、生产签收台账和法务/业务/运营协作包回填表。 + - 脚本不会批准任何复核结果,不会签收证据,也不会自动签署最终上线批准清单。 +- 已更新协作包说明,把真实 reviewer 分配矩阵作为真实复核之前的第一步。 + +### 本轮验证 +- gstack 预检:`GSTACK_OK`。 +- PowerShell Parser 检查: + - `scripts/apply-formal-launch-reviewer-assignment-matrix.ps1` 通过。 +- 当前正式矩阵校验: + - `FORMAL_LAUNCH_REVIEWER_ASSIGNMENT_MATRIX_BLOCKED` + - 产物:`project-docs/qa-artifacts/formal-launch-reviewer-assignment-matrix/2026-05-13T12-08-45-433/formal-launch-reviewer-assignment-matrix-summary.md` + - 结果:`missing_groups=4`,包括 `LEGAL_CORE`、`BUSINESS_TEMPLATE`、`OPS_MATERIAL`、`FINAL_APPROVER`。 +- 临时真实姓名矩阵烟测: + - `FORMAL_LAUNCH_REVIEWER_ASSIGNMENT_MATRIX_PASS` + - 产物:`project-docs/qa-artifacts/formal-launch-reviewer-assignment-matrix/2026-05-13T12-09-07-434/formal-launch-reviewer-assignment-matrix-summary.md` + - 说明:该烟测未使用 `-Apply`,没有修改正式台账。 + +### 当前状态更新 +- 真实 reviewer 分配已经有可执行入口:填矩阵、先校验、再显式 Apply。 +- 当前正式矩阵仍未填写真实人员,因此门禁继续停在真实分配/签收/证据阶段。 + +### 当前阻塞项 +- 需要真实业务输入: + - `LEGAL_CORE.reviewer_name` + - `BUSINESS_TEMPLATE.reviewer_name` + - `OPS_MATERIAL.reviewer_name` + - `FINAL_APPROVER.reviewer_name` +- 填完后再运行分配矩阵脚本 `-Apply`、远程 PostgreSQL 台账导入、数据库真相源批准门禁复跑。 diff --git a/project-docs/formal-launch-ops/reviewers/collaboration-packages/README.md b/project-docs/formal-launch-ops/reviewers/collaboration-packages/README.md index 00b1d57b..c7675325 100644 --- a/project-docs/formal-launch-ops/reviewers/collaboration-packages/README.md +++ b/project-docs/formal-launch-ops/reviewers/collaboration-packages/README.md @@ -13,13 +13,16 @@ ## 使用方式 -1. 真实复核人打开自己目录下的 `return-bundle.csv`。 -2. 填写真实姓名、复核日期、复核状态、批准结论和修改意见。 -3. 打开对应 `evidence/` 目录下的证据文件。 -4. 完成复核后,将 `EVIDENCE_STATUS: TEMPLATE` 改为 `EVIDENCE_STATUS: READY`,并删除所有 `TODO_REAL_REVIEWER`。 -5. 法务、业务、运营包使用 `scripts/import-formal-launch-review-return-bundle.ps1` 校验回填包。 -6. 校验通过后,才可以加 `-Apply` 写回主台账。 -7. `FINAL_APPROVER/` 不使用 CSV 导入脚本;最终批准人填写 `final-launch-approval-checklist.md` 后,通过 `scripts/run-formal-launch-approval-gate.ps1` 统一验证。 +1. 管理员先在 `project-docs/formal-launch-ops/reviewers/reviewer-assignment-matrix.csv` 填写真实法务、业务、运营和最终批准人。 +2. 运行 `scripts/apply-formal-launch-reviewer-assignment-matrix.ps1` 校验分配矩阵;仍有 `TBD`、`PENDING`、`待复核` 等占位时会保持 `BLOCKED`。 +3. 分配矩阵校验通过后,才可以加 `-Apply` 写回复核任务包、签收台账和协作包回填表。 +4. 真实复核人打开自己目录下的 `return-bundle.csv`。 +5. 填写真实姓名、复核日期、复核状态、批准结论和修改意见。 +6. 打开对应 `evidence/` 目录下的证据文件。 +7. 完成复核后,将 `EVIDENCE_STATUS: TEMPLATE` 改为 `EVIDENCE_STATUS: READY`,并删除所有 `TODO_REAL_REVIEWER`。 +8. 法务、业务、运营包使用 `scripts/import-formal-launch-review-return-bundle.ps1` 校验回填包。 +9. 校验通过后,才可以加 `-Apply` 写回主台账。 +10. `FINAL_APPROVER/` 不使用 CSV 导入脚本;最终批准人填写 `final-launch-approval-checklist.md` 后,通过 `scripts/run-formal-launch-approval-gate.ps1` 统一验证。 ## 安全规则 diff --git a/scripts/apply-formal-launch-reviewer-assignment-matrix.ps1 b/scripts/apply-formal-launch-reviewer-assignment-matrix.ps1 new file mode 100644 index 00000000..d22484f5 --- /dev/null +++ b/scripts/apply-formal-launch-reviewer-assignment-matrix.ps1 @@ -0,0 +1,381 @@ +param( + [string]$AssignmentMatrixCsv = "", + [string]$ArtifactRoot = "", + [switch]$Apply +) + +$ErrorActionPreference = "Stop" + +function Get-RepoRoot { + $root = (& git rev-parse --show-toplevel 2>$null) + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($root)) { + throw "unable to resolve git repository root" + } + return $root.Trim() +} + +function New-Utf8NoBomEncoding { + return New-Object System.Text.UTF8Encoding($false) +} + +function Write-Utf8File { + param( + [Parameter(Mandatory = $true)][string]$Path, + [Parameter(Mandatory = $true)][AllowEmptyString()][string]$Text + ) + [System.IO.File]::WriteAllText($Path, $Text, (New-Utf8NoBomEncoding)) +} + +function Import-CsvRows { + param([Parameter(Mandatory = $true)][string]$Path) + if (-not (Test-Path -LiteralPath $Path)) { + return @() + } + return @(Import-Csv -LiteralPath $Path) +} + +function Export-CsvRows { + param( + [Parameter(Mandatory = $true)][AllowEmptyCollection()][object[]]$Rows, + [Parameter(Mandatory = $true)][string]$Path + ) + $Rows | Export-Csv -LiteralPath $Path -NoTypeInformation -Encoding UTF8 +} + +function ConvertFrom-CodePoints { + param([Parameter(Mandatory = $true)][int[]]$CodePoints) + return (-join ($CodePoints | ForEach-Object { [char]$_ })) +} + +$script:CnLegal = ConvertFrom-CodePoints @(0x6cd5, 0x52a1) +$script:CnBusiness = ConvertFrom-CodePoints @(0x4e1a, 0x52a1) +$script:CnOps = ConvertFrom-CodePoints @(0x8fd0, 0x8425) +$script:CnOpsMaintenance = ConvertFrom-CodePoints @(0x8fd0, 0x7ef4) +$script:CnDelivery = ConvertFrom-CodePoints @(0x4ea4, 0x4ed8) +$script:CnOwner = ConvertFrom-CodePoints @(0x8d1f, 0x8d23, 0x4eba) +$script:CnOnlineApprovalOwner = ConvertFrom-CodePoints @(0x4e0a, 0x7ebf, 0x6279, 0x51c6, 0x8d1f, 0x8d23, 0x4eba) +$script:CnPending = ConvertFrom-CodePoints @(0x5f85) +$script:CnReview = ConvertFrom-CodePoints @(0x590d, 0x6838) +$script:CnSignoff = ConvertFrom-CodePoints @(0x7b7e, 0x6536) +$script:CnApproval = ConvertFrom-CodePoints @(0x6279, 0x51c6) +$script:CnConfirm = ConvertFrom-CodePoints @(0x786e, 0x8ba4) + +function Test-RealName { + param([AllowNull()][string]$Value) + $rawValue = ([string]$Value).Trim() + if ([string]::IsNullOrWhiteSpace($rawValue)) { + return $false + } + $normalized = $rawValue.ToUpperInvariant() + $placeholderValues = @( + "TBD", + "TODO", + "TBD_REAL_REVIEWER", + "TBD_REAL_LEGAL_REVIEWER", + "TBD_REAL_BUSINESS_REVIEWER", + "TBD_REAL_OPS_REVIEWER", + "TBD_REAL_LAUNCH_APPROVER", + "UNASSIGNED", + "PENDING", + "$script:CnLegal$script:CnOwner", + "$script:CnBusiness$script:CnOwner", + "$script:CnOps$script:CnOwner", + "$script:CnOpsMaintenance$script:CnOwner", + "$script:CnDelivery$script:CnOwner", + $script:CnOnlineApprovalOwner, + "$script:CnPending$script:CnLegal$script:CnReview", + "$script:CnPending$script:CnBusiness$script:CnReview", + "$script:CnPending$script:CnOps$script:CnReview", + "$script:CnPending$script:CnOnlineApprovalOwner", + "$script:CnPending$script:CnReview", + "$script:CnPending$script:CnSignoff" + ) + if ($placeholderValues -contains $normalized -or $placeholderValues -contains $rawValue) { + return $false + } + if ($normalized.Contains("TODO") -or $normalized.Contains("TBD")) { + return $false + } + if ($rawValue.StartsWith($script:CnPending) -and ($rawValue.Contains($script:CnReview) -or $rawValue.Contains($script:CnSignoff) -or $rawValue.Contains($script:CnApproval) -or $rawValue.Contains($script:CnConfirm))) { + return $false + } + return $true +} + +function Get-ReviewerName { + param( + [Parameter(Mandatory = $true)][hashtable]$Matrix, + [Parameter(Mandatory = $true)][string]$Group + ) + if (-not $Matrix.ContainsKey($Group)) { + return "" + } + return ([string]$Matrix[$Group].reviewer_name).Trim() +} + +function Get-DueDate { + param( + [Parameter(Mandatory = $true)][hashtable]$Matrix, + [Parameter(Mandatory = $true)][string]$Group + ) + if (-not $Matrix.ContainsKey($Group)) { + return "" + } + return ([string]$Matrix[$Group].due_date).Trim() +} + +function Get-ReviewTaskAssignee { + param( + [Parameter(Mandatory = $true)][object]$Row, + [Parameter(Mandatory = $true)][hashtable]$Matrix + ) + $track = (([string]$Row.review_track) + " " + ([string]$Row.required_reviewer_role)).ToLowerInvariant() + $names = New-Object System.Collections.Generic.List[string] + if ($track.Contains("legal") -or $track.Contains($script:CnLegal)) { + $names.Add((Get-ReviewerName -Matrix $Matrix -Group "LEGAL_CORE")) | Out-Null + } + if ($track.Contains("business") -or $track.Contains($script:CnBusiness)) { + $names.Add((Get-ReviewerName -Matrix $Matrix -Group "BUSINESS_TEMPLATE")) | Out-Null + } + if ($track.Contains("ops") -or $track.Contains($script:CnOps) -or $track.Contains($script:CnDelivery)) { + $names.Add((Get-ReviewerName -Matrix $Matrix -Group "OPS_MATERIAL")) | Out-Null + } + if ($track.Contains("final") -or $track.Contains($script:CnApproval)) { + $names.Add((Get-ReviewerName -Matrix $Matrix -Group "FINAL_APPROVER")) | Out-Null + } + $realNames = @($names.ToArray() | Where-Object { Test-RealName $_ } | Select-Object -Unique) + if ($realNames.Count -eq 0) { + return "" + } + return ($realNames -join "; ") +} + +function Set-IfColumnExists { + param( + [Parameter(Mandatory = $true)][object]$Row, + [Parameter(Mandatory = $true)][string]$Column, + [AllowNull()][string]$Value + ) + if ($Row.PSObject.Properties.Name -contains $Column) { + $Row.$Column = $Value + } +} + +$repoRoot = Get-RepoRoot +if ([string]::IsNullOrWhiteSpace($AssignmentMatrixCsv)) { + $AssignmentMatrixCsv = Join-Path $repoRoot "project-docs\formal-launch-ops\reviewers\reviewer-assignment-matrix.csv" +} +if (-not [System.IO.Path]::IsPathRooted($AssignmentMatrixCsv)) { + $AssignmentMatrixCsv = Join-Path $repoRoot $AssignmentMatrixCsv +} + +$timestamp = Get-Date -Format "yyyy-MM-ddTHH-mm-ss-fff" +if ([string]::IsNullOrWhiteSpace($ArtifactRoot)) { + $ArtifactRoot = Join-Path $repoRoot "project-docs\qa-artifacts\formal-launch-reviewer-assignment-matrix\$timestamp" +} +if (-not [System.IO.Path]::IsPathRooted($ArtifactRoot)) { + $ArtifactRoot = Join-Path $repoRoot $ArtifactRoot +} +New-Item -ItemType Directory -Force -Path $ArtifactRoot | Out-Null + +$summaryJson = Join-Path $ArtifactRoot "formal-launch-reviewer-assignment-matrix-summary.json" +$summaryMd = Join-Path $ArtifactRoot "formal-launch-reviewer-assignment-matrix-summary.md" +$errorsPath = Join-Path $ArtifactRoot "formal-launch-reviewer-assignment-matrix-errors.txt" +$warningsPath = Join-Path $ArtifactRoot "formal-launch-reviewer-assignment-matrix-warnings.txt" + +$reviewTaskPath = Join-Path $repoRoot "project-docs\formal-launch-ops\reviewers\real-review-task-package.csv" +$materialSignoffPath = Join-Path $repoRoot "project-docs\formal-launch-ops\signoff\production-material-signoff-register.csv" +$templateSignoffPath = Join-Path $repoRoot "project-docs\formal-launch-ops\signoff\production-template-signoff-register.csv" +$knowledgeSignoffPath = Join-Path $repoRoot "project-docs\formal-launch-ops\signoff\production-knowledge-signoff-register.csv" +$legalBundlePath = Join-Path $repoRoot "project-docs\formal-launch-ops\reviewers\collaboration-packages\LEGAL_CORE\return-bundle.csv" +$businessBundlePath = Join-Path $repoRoot "project-docs\formal-launch-ops\reviewers\collaboration-packages\BUSINESS_TEMPLATE\return-bundle.csv" +$opsBundlePath = Join-Path $repoRoot "project-docs\formal-launch-ops\reviewers\collaboration-packages\OPS_MATERIAL\return-bundle.csv" + +$requiredGroups = @("LEGAL_CORE", "BUSINESS_TEMPLATE", "OPS_MATERIAL", "FINAL_APPROVER") +$errors = New-Object System.Collections.Generic.List[string] +$warnings = New-Object System.Collections.Generic.List[string] + +$matrixRows = Import-CsvRows -Path $AssignmentMatrixCsv +if ($matrixRows.Count -eq 0) { + $errors.Add("assignment matrix has no rows: $AssignmentMatrixCsv") | Out-Null +} + +$matrix = @{} +foreach ($row in $matrixRows) { + $group = ([string]$row.assignment_group).Trim() + if (-not [string]::IsNullOrWhiteSpace($group)) { + $matrix[$group] = $row + } +} + +$missingGroups = New-Object System.Collections.Generic.List[string] +foreach ($group in $requiredGroups) { + if (-not $matrix.ContainsKey($group)) { + $errors.Add("assignment group missing: $group") | Out-Null + $missingGroups.Add($group) | Out-Null + continue + } + $reviewerName = ([string]$matrix[$group].reviewer_name).Trim() + if (-not (Test-RealName $reviewerName)) { + $missingGroups.Add($group) | Out-Null + } + if ([string]::IsNullOrWhiteSpace(([string]$matrix[$group].due_date).Trim())) { + $warnings.Add("assignment group $group has no due_date") | Out-Null + } +} + +$changedFiles = New-Object System.Collections.Generic.List[string] +if ($Apply -and $errors.Count -eq 0 -and $missingGroups.Count -eq 0) { + $legalName = Get-ReviewerName -Matrix $matrix -Group "LEGAL_CORE" + $businessName = Get-ReviewerName -Matrix $matrix -Group "BUSINESS_TEMPLATE" + $opsName = Get-ReviewerName -Matrix $matrix -Group "OPS_MATERIAL" + $finalName = Get-ReviewerName -Matrix $matrix -Group "FINAL_APPROVER" + + foreach ($row in $matrixRows) { + if ($requiredGroups -contains ([string]$row.assignment_group).Trim()) { + Set-IfColumnExists -Row $row -Column "status" -Value "ASSIGNED" + } + } + Export-CsvRows -Rows $matrixRows -Path $AssignmentMatrixCsv + $changedFiles.Add($AssignmentMatrixCsv) | Out-Null + + $reviewTasks = Import-CsvRows -Path $reviewTaskPath + foreach ($row in $reviewTasks) { + $assignee = Get-ReviewTaskAssignee -Row $row -Matrix $matrix + if (Test-RealName $assignee) { + Set-IfColumnExists -Row $row -Column "assigned_reviewer" -Value $assignee + $dueDates = @( + Get-DueDate -Matrix $matrix -Group "LEGAL_CORE", + Get-DueDate -Matrix $matrix -Group "BUSINESS_TEMPLATE", + Get-DueDate -Matrix $matrix -Group "OPS_MATERIAL" + ) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique + if ($dueDates.Count -gt 0) { + Set-IfColumnExists -Row $row -Column "due_date" -Value ($dueDates -join "; ") + } + } + } + Export-CsvRows -Rows $reviewTasks -Path $reviewTaskPath + $changedFiles.Add($reviewTaskPath) | Out-Null + + $materialSignoffs = Import-CsvRows -Path $materialSignoffPath + foreach ($row in $materialSignoffs) { + $materialId = ([string]$row.material_id).Trim() + $roleText = (([string]$row.signoff_role) + " " + ([string]$row.owner) + " " + $materialId) + $name = $opsName + if ($roleText.Contains($script:CnLegal) -or @("MAT-USER-AGREEMENT-001", "MAT-PRIVACY-POLICY-001", "MAT-AI-DISCLAIMER-001", "MAT-LEGAL-REVIEW-SOP-001") -contains $materialId) { + $name = $legalName + } + Set-IfColumnExists -Row $row -Column "signer_name" -Value $name + } + Export-CsvRows -Rows $materialSignoffs -Path $materialSignoffPath + $changedFiles.Add($materialSignoffPath) | Out-Null + + $templateSignoffs = Import-CsvRows -Path $templateSignoffPath + foreach ($row in $templateSignoffs) { + Set-IfColumnExists -Row $row -Column "legal_reviewer" -Value $legalName + Set-IfColumnExists -Row $row -Column "business_reviewer" -Value $businessName + } + Export-CsvRows -Rows $templateSignoffs -Path $templateSignoffPath + $changedFiles.Add($templateSignoffPath) | Out-Null + + $knowledgeSignoffs = Import-CsvRows -Path $knowledgeSignoffPath + foreach ($row in $knowledgeSignoffs) { + Set-IfColumnExists -Row $row -Column "reviewer" -Value $legalName + } + Export-CsvRows -Rows $knowledgeSignoffs -Path $knowledgeSignoffPath + $changedFiles.Add($knowledgeSignoffPath) | Out-Null + + foreach ($bundlePath in @($legalBundlePath, $businessBundlePath, $opsBundlePath)) { + if (-not (Test-Path -LiteralPath $bundlePath)) { + continue + } + $bundleRows = Import-CsvRows -Path $bundlePath + foreach ($row in $bundleRows) { + if ($bundlePath -eq $legalBundlePath) { + Set-IfColumnExists -Row $row -Column "reviewer_name" -Value $legalName + Set-IfColumnExists -Row $row -Column "legal_reviewer" -Value $legalName + Set-IfColumnExists -Row $row -Column "signer_name" -Value $legalName + } elseif ($bundlePath -eq $businessBundlePath) { + Set-IfColumnExists -Row $row -Column "reviewer_name" -Value $businessName + Set-IfColumnExists -Row $row -Column "legal_reviewer" -Value $legalName + Set-IfColumnExists -Row $row -Column "business_reviewer" -Value $businessName + } else { + Set-IfColumnExists -Row $row -Column "reviewer_name" -Value $opsName + Set-IfColumnExists -Row $row -Column "signer_name" -Value $opsName + } + } + Export-CsvRows -Rows $bundleRows -Path $bundlePath + $changedFiles.Add($bundlePath) | Out-Null + } + + $warnings.Add("FINAL_APPROVER checklist is not auto-signed; final approval must be completed after all review and signoff evidence is ready. Final approver candidate: $finalName") | Out-Null +} + +$status = if ($errors.Count -gt 0) { + "FAIL" +} elseif ($missingGroups.Count -gt 0) { + "BLOCKED" +} else { + "PASS" +} + +Write-Utf8File -Path $errorsPath -Text (($errors.ToArray()) -join [Environment]::NewLine) +Write-Utf8File -Path $warningsPath -Text (($warnings.ToArray()) -join [Environment]::NewLine) + +$summary = [ordered]@{ + status = $status + generated_at = (Get-Date).ToString("o") + apply = [bool]$Apply + assignment_matrix = $AssignmentMatrixCsv + metrics = [ordered]@{ + required_groups = $requiredGroups.Count + assigned_groups = ($requiredGroups.Count - $missingGroups.Count) + missing_groups = $missingGroups.Count + errors = $errors.Count + warnings = $warnings.Count + changed_files = $changedFiles.Count + } + missing_groups = $missingGroups.ToArray() + changed_files = $changedFiles.ToArray() + artifacts = [ordered]@{ + summary_json = $summaryJson + summary_md = $summaryMd + errors = $errorsPath + warnings = $warningsPath + } +} +$summary | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $summaryJson -Encoding UTF8 + +$md = New-Object System.Collections.Generic.List[string] +$md.Add("# Formal Launch Reviewer Assignment Matrix Summary") | Out-Null +$md.Add("") | Out-Null +$md.Add("- status: $status") | Out-Null +$md.Add("- apply: $([bool]$Apply)") | Out-Null +$md.Add("- assignment_matrix: $AssignmentMatrixCsv") | Out-Null +$md.Add("") | Out-Null +$md.Add("## Metrics") | Out-Null +$md.Add("") | Out-Null +$md.Add("| metric | value |") | Out-Null +$md.Add("|---|---:|") | Out-Null +foreach ($property in $summary.metrics.GetEnumerator()) { + $md.Add("| $($property.Key) | $($property.Value) |") | Out-Null +} +$md.Add("") | Out-Null +$md.Add("## Missing Groups") | Out-Null +$md.Add("") | Out-Null +if ($missingGroups.Count -eq 0) { + $md.Add("- none") | Out-Null +} else { + foreach ($group in $missingGroups) { + $md.Add("- $group") | Out-Null + } +} +$md.Add("") | Out-Null +$md.Add("This script never approves review results or evidence. It only validates or applies real reviewer assignment names from the assignment matrix.") | Out-Null +Write-Utf8File -Path $summaryMd -Text ($md -join [Environment]::NewLine) + +Write-Output "FORMAL_LAUNCH_REVIEWER_ASSIGNMENT_MATRIX_$status $ArtifactRoot" + +if ($status -eq "FAIL") { + exit 1 +}