feat: add reviewer assignment matrix gate
This commit is contained in:
@@ -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 台账导入、数据库真相源批准门禁复跑。
|
||||
|
||||
@@ -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` 统一验证。
|
||||
|
||||
## 安全规则
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user