Files
ai-contract-platform/scripts/run-formal-launch-approval-gate.ps1
T

612 lines
31 KiB
PowerShell

param(
[string]$ArtifactRoot = "",
[int]$MinReviewTasks = 90,
[int]$MinApprovedReviewTasks = 31,
[int]$MinApprovedMaterials = 7,
[int]$MinApprovedTemplates = 5,
[int]$MinApprovedKnowledgeCards = 10,
[int]$MinApprovedReviewRules = 8,
[int]$MinApprovedClauses = 8,
[string]$TenantId = "tenant-demo-001",
[string]$DatabaseUrl = "",
[ValidateSet("none", "sqlite_governance")]
[string]$DatabaseBootstrapMode = "none",
[switch]$SkipPreparationGate,
[switch]$SkipDatabaseState,
[switch]$RequireDatabaseTruth,
[switch]$FailOnBlocked
)
$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 Test-TrueValue {
param([AllowNull()][string]$Value)
return @("true", "1", "yes", "y") -contains ([string]$Value).Trim().ToLowerInvariant()
}
function Test-ApprovedValue {
param([AllowNull()][string]$Value)
$normalized = ([string]$Value).Trim().ToUpperInvariant()
return @("APPROVED", "SIGNED", "PASS", "PRODUCTION_READY") -contains $normalized
}
function Test-AssignedReviewer {
param([AllowNull()][string]$Value)
$normalized = ([string]$Value).Trim().ToUpperInvariant()
if ([string]::IsNullOrWhiteSpace($normalized)) {
return $false
}
return -not (@("TBD", "TBD_REAL_REVIEWER", "PENDING", "UNASSIGNED") -contains $normalized)
}
function Test-EvidenceAvailable {
param([AllowNull()][string]$Value)
$path = ([string]$Value).Trim()
if ([string]::IsNullOrWhiteSpace($path)) {
return $false
}
if ($path -match "^https?://") {
return $true
}
if ([System.IO.Path]::IsPathRooted($path)) {
if (-not (Test-Path -LiteralPath $path)) {
return $false
}
return (Test-EvidenceContentFinal -Path $path)
}
$resolvedPath = Join-Path $repoRoot $path
if (-not (Test-Path -LiteralPath $resolvedPath)) {
return $false
}
return (Test-EvidenceContentFinal -Path $resolvedPath)
}
function Test-EvidenceContentFinal {
param([Parameter(Mandatory = $true)][string]$Path)
$item = Get-Item -LiteralPath $Path -ErrorAction Stop
if ($item.PSIsContainer) {
return $false
}
$content = Get-Content -LiteralPath $Path -Raw -ErrorAction Stop
if ($content -match "EVIDENCE_STATUS:\s*TEMPLATE") {
return $false
}
if ($content -match "TODO_REAL_REVIEWER") {
return $false
}
return $true
}
function Get-NumericPropertyValue {
param(
[object]$Object,
[string]$PropertyName,
[int]$Default = 0
)
if ($null -eq $Object) {
return $Default
}
$property = $Object.PSObject.Properties[$PropertyName]
if ($null -eq $property) {
return $Default
}
return [int]$property.Value
}
function Add-Check {
param(
[Parameter(Mandatory = $true)][string]$Key,
[Parameter(Mandatory = $true)][string]$Label,
[Parameter(Mandatory = $true)]
[ValidateSet("pass", "fail", "warn", "skip", "blocked")]
[string]$Status,
[string]$Detail = "",
[bool]$Required = $true,
[hashtable]$Meta = @{}
)
$script:checks.Add([ordered]@{
key = $Key
label = $Label
status = $Status
required = $Required
detail = $Detail
meta = $Meta
}) | Out-Null
}
function Invoke-PreparationGate {
param(
[Parameter(Mandatory = $true)][string]$RepoRoot,
[Parameter(Mandatory = $true)][string]$ArtifactDir
)
$scriptPath = Join-Path $RepoRoot "scripts\run-formal-launch-preparation-gate.ps1"
$logPath = Join-Path $ArtifactDir "formal-launch-preparation-gate.log"
$childArtifact = Join-Path $ArtifactDir "formal-launch-preparation"
if (-not (Test-Path -LiteralPath $scriptPath)) {
Add-Check -Key "preparation_gate" -Label "formal launch preparation gate" -Status "fail" -Detail "missing preparation gate script" -Meta @{ log = $logPath }
return
}
$output = & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $scriptPath -ArtifactRoot $childArtifact 2>&1
$exitCode = $LASTEXITCODE
Write-Utf8File -Path $logPath -Text ($output -join [Environment]::NewLine)
$statusLine = @($output | Where-Object { $_ -match "^FORMAL_LAUNCH_PREPARATION_GATE_" } | Select-Object -Last 1)
$status = "UNKNOWN"
if ($statusLine.Count -gt 0 -and $statusLine[-1] -match "^FORMAL_LAUNCH_PREPARATION_GATE_([A-Z_]+)") {
$status = $Matches[1]
}
if ($exitCode -ne 0) {
Add-Check -Key "preparation_gate" -Label "formal launch preparation gate" -Status "fail" -Detail "child failed: status=$status exit=$exitCode" -Meta @{ log = $logPath; artifact = $childArtifact }
} elseif ($status -eq "PASS") {
Add-Check -Key "preparation_gate" -Label "formal launch preparation gate" -Status "pass" -Detail "preparation gate passed" -Meta @{ log = $logPath; artifact = $childArtifact }
} elseif ($status -eq "WARN") {
Add-Check -Key "preparation_gate" -Label "formal launch preparation gate" -Status "warn" -Detail "preparation gate warned" -Meta @{ log = $logPath; artifact = $childArtifact }
} elseif ($status -eq "BLOCKED") {
Add-Check -Key "preparation_gate" -Label "formal launch preparation gate" -Status "blocked" -Detail "preparation gate blocked" -Meta @{ log = $logPath; artifact = $childArtifact }
} else {
Add-Check -Key "preparation_gate" -Label "formal launch preparation gate" -Status "fail" -Detail "unexpected preparation status=$status exit=$exitCode" -Meta @{ log = $logPath; artifact = $childArtifact }
}
}
function Invoke-DatabaseSnapshot {
param(
[Parameter(Mandatory = $true)][string]$RepoRoot,
[Parameter(Mandatory = $true)][string]$ArtifactDir,
[Parameter(Mandatory = $true)][string]$TenantId,
[string]$DatabaseUrl = "",
[string]$BootstrapMode = "none"
)
$scriptPath = Join-Path $RepoRoot "scripts\export-formal-launch-governance-db-state.ps1"
$logPath = Join-Path $ArtifactDir "formal-launch-governance-db-state.log"
$childArtifact = Join-Path $ArtifactDir "formal-launch-governance-db-state"
if (-not (Test-Path -LiteralPath $scriptPath)) {
return [ordered]@{
available = $false
exit_code = 1
status = "FAIL"
log = $logPath
artifact = $childArtifact
payload = $null
reason = "missing database state export script"
}
}
$arguments = @(
"-ArtifactRoot", $childArtifact,
"-TenantId", $TenantId,
"-DisplayName", "formal-launch-approval-gate",
"-BootstrapMode", $BootstrapMode
)
if (-not [string]::IsNullOrWhiteSpace($DatabaseUrl)) {
$arguments += @("-DatabaseUrl", $DatabaseUrl)
}
$previousErrorActionPreference = $ErrorActionPreference
$ErrorActionPreference = "Continue"
try {
$output = & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $scriptPath @arguments 2>&1
$exitCode = $LASTEXITCODE
} finally {
$ErrorActionPreference = $previousErrorActionPreference
}
Write-Utf8File -Path $logPath -Text ($output -join [Environment]::NewLine)
$statusLine = @($output | Where-Object { $_ -match "^FORMAL_LAUNCH_GOVERNANCE_DB_STATE_" } | Select-Object -Last 1)
$status = "FAIL"
if ($statusLine.Count -gt 0 -and $statusLine[-1] -match "^FORMAL_LAUNCH_GOVERNANCE_DB_STATE_([A-Z]+)\s+(.+)$") {
$status = $Matches[1]
$childArtifact = $Matches[2].Trim()
}
$summaryPath = Join-Path $childArtifact "formal-launch-governance-db-state-summary.json"
$payload = $null
if (Test-Path -LiteralPath $summaryPath) {
$payload = Get-Content -LiteralPath $summaryPath -Raw | ConvertFrom-Json
}
return [ordered]@{
available = ($exitCode -eq 0 -and $null -ne $payload)
exit_code = $exitCode
status = $status
log = $logPath
artifact = $childArtifact
payload = $payload
reason = $null
}
}
$repoRoot = Get-RepoRoot
$timestamp = Get-Date -Format "yyyy-MM-ddTHH-mm-ss-fff"
if ([string]::IsNullOrWhiteSpace($ArtifactRoot)) {
$ArtifactRoot = Join-Path $repoRoot "project-docs\qa-artifacts\formal-launch-approval-gate\$timestamp"
}
New-Item -ItemType Directory -Force -Path $ArtifactRoot | Out-Null
$checks = New-Object System.Collections.Generic.List[object]
$checksCsv = Join-Path $ArtifactRoot "formal-launch-approval-checks.csv"
$summaryJson = Join-Path $ArtifactRoot "formal-launch-approval-summary.json"
$summaryMd = Join-Path $ArtifactRoot "formal-launch-approval-summary.md"
$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"
if (-not (Test-Path "$HOME\.Codex\skills\gstack\bin")) {
Add-Check -Key "gstack" -Label "gstack preflight" -Status "fail" -Detail "gstack missing"
} else {
Add-Check -Key "gstack" -Label "gstack preflight" -Status "pass" -Detail "GSTACK_OK"
}
$requiredInputs = @(
@{ key = "review_task_package"; label = "real review task package"; path = $reviewTaskPath },
@{ key = "material_signoff_register"; label = "material signoff register"; path = $materialSignoffPath },
@{ key = "template_signoff_register"; label = "template signoff register"; path = $templateSignoffPath },
@{ key = "knowledge_signoff_register"; label = "knowledge signoff register"; path = $knowledgeSignoffPath }
)
if ($SkipPreparationGate) {
Add-Check -Key "preparation_gate" -Label "formal launch preparation gate" -Status "skip" -Required $false -Detail "skipped by parameter"
} else {
Invoke-PreparationGate -RepoRoot $repoRoot -ArtifactDir $ArtifactRoot
}
$dataSource = "csv"
$databaseSnapshot = $null
$databaseSnapshotUsable = $false
if ($SkipDatabaseState) {
Add-Check -Key "database_truth_source" -Label "database truth source" -Status "skip" -Required $false -Detail "skipped by parameter"
} else {
$databaseSnapshot = Invoke-DatabaseSnapshot -RepoRoot $repoRoot -ArtifactDir $ArtifactRoot -TenantId $TenantId -DatabaseUrl $DatabaseUrl -BootstrapMode $DatabaseBootstrapMode
if ($databaseSnapshot.available) {
$dbTotalAssets = Get-NumericPropertyValue -Object $databaseSnapshot.payload.data.summary -PropertyName "total_assets"
if ($dbTotalAssets -gt 0) {
$databaseSnapshotUsable = $true
$dataSource = "database"
Add-Check -Key "database_truth_source" -Label "database truth source" -Status "pass" -Detail "database snapshot available; source=database; snapshot_status=$($databaseSnapshot.status)" -Meta @{ log = $databaseSnapshot.log; artifact = $databaseSnapshot.artifact }
} elseif ($RequireDatabaseTruth) {
Add-Check -Key "database_truth_source" -Label "database truth source" -Status "blocked" -Detail "database snapshot has no imported governance assets" -Meta @{ log = $databaseSnapshot.log; artifact = $databaseSnapshot.artifact }
} else {
Add-Check -Key "database_truth_source" -Label "database truth source" -Status "warn" -Required $false -Detail "database snapshot has no imported governance assets; fallback=csv" -Meta @{ log = $databaseSnapshot.log; artifact = $databaseSnapshot.artifact }
}
} elseif ($RequireDatabaseTruth) {
Add-Check -Key "database_truth_source" -Label "database truth source" -Status "fail" -Detail "database snapshot unavailable and -RequireDatabaseTruth is set" -Meta @{ log = $databaseSnapshot.log; artifact = $databaseSnapshot.artifact; exit_code = $databaseSnapshot.exit_code }
} else {
Add-Check -Key "database_truth_source" -Label "database truth source" -Status "warn" -Required $false -Detail "database snapshot unavailable; fallback=csv" -Meta @{ log = $databaseSnapshot.log; artifact = $databaseSnapshot.artifact; exit_code = $databaseSnapshot.exit_code }
}
}
$reviewTasks = @()
$materialSignoffs = @()
$templateSignoffs = @()
$knowledgeSignoffs = @()
if ($dataSource -eq "database") {
foreach ($input in $requiredInputs) {
Add-Check -Key $input.key -Label $input.label -Status "skip" -Required $false -Detail "database source active; csv input check skipped" -Meta @{ path = $input.path }
}
} else {
foreach ($input in $requiredInputs) {
if (Test-Path -LiteralPath $input.path) {
Add-Check -Key $input.key -Label $input.label -Status "pass" -Detail "input exists" -Meta @{ path = $input.path }
} else {
Add-Check -Key $input.key -Label $input.label -Status "fail" -Detail "input missing" -Meta @{ path = $input.path }
}
}
$reviewTasks = Import-CsvRows -Path $reviewTaskPath
$materialSignoffs = Import-CsvRows -Path $materialSignoffPath
$templateSignoffs = Import-CsvRows -Path $templateSignoffPath
$knowledgeSignoffs = Import-CsvRows -Path $knowledgeSignoffPath
}
$gateMetrics = if ($dataSource -eq "database") {
$approvalMetrics = $databaseSnapshot.payload.data.approval_metrics
$assetTypes = $databaseSnapshot.payload.data.asset_types
[ordered]@{
review_tasks = Get-NumericPropertyValue -Object $approvalMetrics -PropertyName "review_tasks"
required_review_tasks = Get-NumericPropertyValue -Object $approvalMetrics -PropertyName "required_review_tasks"
missing_reviewer_tasks = Get-NumericPropertyValue -Object $approvalMetrics -PropertyName "missing_reviewer_tasks"
approved_review_tasks = Get-NumericPropertyValue -Object $approvalMetrics -PropertyName "approved_review_tasks"
approved_review_tasks_missing_evidence = Get-NumericPropertyValue -Object $approvalMetrics -PropertyName "approved_review_tasks_missing_evidence"
material_signoffs = Get-NumericPropertyValue -Object $assetTypes -PropertyName "production_material"
approved_materials = Get-NumericPropertyValue -Object $approvalMetrics -PropertyName "approved_materials"
approved_materials_missing_evidence = Get-NumericPropertyValue -Object $approvalMetrics -PropertyName "approved_materials_missing_evidence"
approved_public_materials = Get-NumericPropertyValue -Object $approvalMetrics -PropertyName "approved_public_materials"
approved_public_materials_missing_evidence = Get-NumericPropertyValue -Object $approvalMetrics -PropertyName "approved_public_materials_missing_evidence"
template_signoffs = Get-NumericPropertyValue -Object $assetTypes -PropertyName "contract_template"
approved_templates = Get-NumericPropertyValue -Object $approvalMetrics -PropertyName "approved_templates"
approved_templates_missing_evidence = Get-NumericPropertyValue -Object $approvalMetrics -PropertyName "approved_templates_missing_evidence"
knowledge_signoffs = (Get-NumericPropertyValue -Object $assetTypes -PropertyName "legal_knowledge_card") + (Get-NumericPropertyValue -Object $assetTypes -PropertyName "review_risk_rule") + (Get-NumericPropertyValue -Object $assetTypes -PropertyName "clause")
approved_knowledge_cards = Get-NumericPropertyValue -Object $approvalMetrics -PropertyName "approved_knowledge_cards"
approved_knowledge_cards_missing_evidence = Get-NumericPropertyValue -Object $approvalMetrics -PropertyName "approved_knowledge_cards_missing_evidence"
approved_review_rules = Get-NumericPropertyValue -Object $approvalMetrics -PropertyName "approved_review_rules"
approved_review_rules_missing_evidence = Get-NumericPropertyValue -Object $approvalMetrics -PropertyName "approved_review_rules_missing_evidence"
approved_clauses = Get-NumericPropertyValue -Object $approvalMetrics -PropertyName "approved_clauses"
approved_clauses_missing_evidence = Get-NumericPropertyValue -Object $approvalMetrics -PropertyName "approved_clauses_missing_evidence"
}
} else {
$requiredReviewTasks = @($reviewTasks | Where-Object { Test-TrueValue $_.blocking_before_launch })
$missingReviewerTasks = @($requiredReviewTasks | Where-Object { -not (Test-AssignedReviewer $_.assigned_reviewer) })
$approvedReviewTasks = @($requiredReviewTasks | Where-Object { (Test-ApprovedValue $_.review_status) -and (Test-ApprovedValue $_.approval_result) })
$approvedMaterials = @($materialSignoffs | Where-Object { (Test-ApprovedValue $_.signoff_result) -and (Test-ApprovedValue $_.status) })
$approvedTemplates = @($templateSignoffs | Where-Object { (Test-ApprovedValue $_.legal_signoff_result) -and (Test-ApprovedValue $_.business_signoff_result) -and (Test-ApprovedValue $_.production_approval_result) })
$approvedKnowledgeCards = @($knowledgeSignoffs | Where-Object { $_.asset_type -eq "legal_knowledge_card" -and (Test-ApprovedValue $_.legal_signoff_result) -and (Test-ApprovedValue $_.production_approval_result) })
$approvedReviewRules = @($knowledgeSignoffs | Where-Object { $_.asset_type -eq "review_risk_rule" -and (Test-ApprovedValue $_.legal_signoff_result) -and (Test-ApprovedValue $_.production_approval_result) })
$approvedClauses = @($knowledgeSignoffs | Where-Object { $_.asset_type -eq "clause" -and (Test-ApprovedValue $_.legal_signoff_result) -and (Test-ApprovedValue $_.production_approval_result) })
$approvedReviewTasksMissingEvidence = @($approvedReviewTasks | Where-Object { -not (Test-EvidenceAvailable $_.evidence_path) })
$approvedMaterialsMissingEvidence = @($approvedMaterials | Where-Object { -not (Test-EvidenceAvailable $_.evidence_path) })
$approvedTemplatesMissingEvidence = @($approvedTemplates | Where-Object { -not (Test-EvidenceAvailable $_.evidence_path) })
$approvedKnowledgeCardsMissingEvidence = @($approvedKnowledgeCards | Where-Object { -not (Test-EvidenceAvailable $_.evidence_path) })
$approvedReviewRulesMissingEvidence = @($approvedReviewRules | Where-Object { -not (Test-EvidenceAvailable $_.evidence_path) })
$approvedClausesMissingEvidence = @($approvedClauses | Where-Object { -not (Test-EvidenceAvailable $_.evidence_path) })
$publicMaterialIds = @("MAT-USER-AGREEMENT-001", "MAT-PRIVACY-POLICY-001", "MAT-AI-DISCLAIMER-001")
$approvedPublicMaterials = @($materialSignoffs | Where-Object { $publicMaterialIds -contains $_.material_id -and (Test-ApprovedValue $_.signoff_result) -and (Test-ApprovedValue $_.status) })
$approvedPublicMaterialsMissingEvidence = @($approvedPublicMaterials | Where-Object { -not (Test-EvidenceAvailable $_.evidence_path) })
[ordered]@{
review_tasks = $reviewTasks.Count
required_review_tasks = $requiredReviewTasks.Count
missing_reviewer_tasks = $missingReviewerTasks.Count
approved_review_tasks = $approvedReviewTasks.Count
approved_review_tasks_missing_evidence = $approvedReviewTasksMissingEvidence.Count
material_signoffs = $materialSignoffs.Count
approved_materials = $approvedMaterials.Count
approved_materials_missing_evidence = $approvedMaterialsMissingEvidence.Count
approved_public_materials = $approvedPublicMaterials.Count
approved_public_materials_missing_evidence = $approvedPublicMaterialsMissingEvidence.Count
template_signoffs = $templateSignoffs.Count
approved_templates = $approvedTemplates.Count
approved_templates_missing_evidence = $approvedTemplatesMissingEvidence.Count
knowledge_signoffs = $knowledgeSignoffs.Count
approved_knowledge_cards = $approvedKnowledgeCards.Count
approved_knowledge_cards_missing_evidence = $approvedKnowledgeCardsMissingEvidence.Count
approved_review_rules = $approvedReviewRules.Count
approved_review_rules_missing_evidence = $approvedReviewRulesMissingEvidence.Count
approved_clauses = $approvedClauses.Count
approved_clauses_missing_evidence = $approvedClausesMissingEvidence.Count
}
}
if ($gateMetrics.review_tasks -ge $MinReviewTasks) {
Add-Check -Key "review_task_package_size" -Label "review task package size" -Status "pass" -Detail "review_tasks=$($gateMetrics.review_tasks); min=$MinReviewTasks"
} else {
Add-Check -Key "review_task_package_size" -Label "review task package size" -Status "blocked" -Detail "review_tasks=$($gateMetrics.review_tasks); min=$MinReviewTasks"
}
if ($gateMetrics.missing_reviewer_tasks -eq 0 -and $gateMetrics.required_review_tasks -gt 0) {
Add-Check -Key "real_reviewer_assignment" -Label "real reviewer assignment" -Status "pass" -Detail "all blocking review tasks have assigned reviewers"
} else {
Add-Check -Key "real_reviewer_assignment" -Label "real reviewer assignment" -Status "blocked" -Detail "missing_assigned_reviewers=$($gateMetrics.missing_reviewer_tasks)"
}
if ($gateMetrics.approved_review_tasks -ge $MinApprovedReviewTasks) {
Add-Check -Key "real_review_execution" -Label "real review execution" -Status "pass" -Detail "approved_review_tasks=$($gateMetrics.approved_review_tasks); min=$MinApprovedReviewTasks"
} else {
Add-Check -Key "real_review_execution" -Label "real review execution" -Status "blocked" -Detail "approved_review_tasks=$($gateMetrics.approved_review_tasks); min=$MinApprovedReviewTasks"
}
if ($gateMetrics.approved_review_tasks_missing_evidence -eq 0) {
Add-Check -Key "real_review_evidence" -Label "real review evidence" -Status "pass" -Detail "approved review tasks have evidence"
} else {
Add-Check -Key "real_review_evidence" -Label "real review evidence" -Status "blocked" -Detail "approved_review_tasks_missing_evidence=$($gateMetrics.approved_review_tasks_missing_evidence)"
}
if ($gateMetrics.approved_materials -ge $MinApprovedMaterials) {
Add-Check -Key "material_signoff" -Label "production material signoff" -Status "pass" -Detail "approved_materials=$($gateMetrics.approved_materials); min=$MinApprovedMaterials"
} else {
Add-Check -Key "material_signoff" -Label "production material signoff" -Status "blocked" -Detail "approved_materials=$($gateMetrics.approved_materials); min=$MinApprovedMaterials"
}
if ($gateMetrics.approved_materials_missing_evidence -eq 0) {
Add-Check -Key "material_signoff_evidence" -Label "material signoff evidence" -Status "pass" -Detail "approved materials have evidence"
} else {
Add-Check -Key "material_signoff_evidence" -Label "material signoff evidence" -Status "blocked" -Detail "approved_materials_missing_evidence=$($gateMetrics.approved_materials_missing_evidence)"
}
if ($gateMetrics.approved_public_materials -eq 3) {
Add-Check -Key "public_legal_document_signoff" -Label "public legal document signoff" -Status "pass" -Detail "approved_public_materials=$($gateMetrics.approved_public_materials)"
} else {
Add-Check -Key "public_legal_document_signoff" -Label "public legal document signoff" -Status "blocked" -Detail "approved_public_materials=$($gateMetrics.approved_public_materials); required=3"
}
if ($gateMetrics.approved_public_materials_missing_evidence -eq 0) {
Add-Check -Key "public_legal_document_evidence" -Label "public legal document evidence" -Status "pass" -Detail "approved public materials have evidence"
} else {
Add-Check -Key "public_legal_document_evidence" -Label "public legal document evidence" -Status "blocked" -Detail "approved_public_materials_missing_evidence=$($gateMetrics.approved_public_materials_missing_evidence)"
}
if ($gateMetrics.approved_templates -ge $MinApprovedTemplates) {
Add-Check -Key "template_signoff" -Label "production template signoff" -Status "pass" -Detail "approved_templates=$($gateMetrics.approved_templates); min=$MinApprovedTemplates"
} else {
Add-Check -Key "template_signoff" -Label "production template signoff" -Status "blocked" -Detail "approved_templates=$($gateMetrics.approved_templates); min=$MinApprovedTemplates"
}
if ($gateMetrics.approved_templates_missing_evidence -eq 0) {
Add-Check -Key "template_signoff_evidence" -Label "template signoff evidence" -Status "pass" -Detail "approved templates have evidence"
} else {
Add-Check -Key "template_signoff_evidence" -Label "template signoff evidence" -Status "blocked" -Detail "approved_templates_missing_evidence=$($gateMetrics.approved_templates_missing_evidence)"
}
if ($gateMetrics.approved_knowledge_cards -ge $MinApprovedKnowledgeCards) {
Add-Check -Key "knowledge_card_signoff" -Label "knowledge card signoff" -Status "pass" -Detail "approved_knowledge_cards=$($gateMetrics.approved_knowledge_cards); min=$MinApprovedKnowledgeCards"
} else {
Add-Check -Key "knowledge_card_signoff" -Label "knowledge card signoff" -Status "blocked" -Detail "approved_knowledge_cards=$($gateMetrics.approved_knowledge_cards); min=$MinApprovedKnowledgeCards"
}
if ($gateMetrics.approved_knowledge_cards_missing_evidence -eq 0) {
Add-Check -Key "knowledge_card_evidence" -Label "knowledge card evidence" -Status "pass" -Detail "approved knowledge cards have evidence"
} else {
Add-Check -Key "knowledge_card_evidence" -Label "knowledge card evidence" -Status "blocked" -Detail "approved_knowledge_cards_missing_evidence=$($gateMetrics.approved_knowledge_cards_missing_evidence)"
}
if ($gateMetrics.approved_review_rules -ge $MinApprovedReviewRules) {
Add-Check -Key "review_rule_signoff" -Label "review rule signoff" -Status "pass" -Detail "approved_review_rules=$($gateMetrics.approved_review_rules); min=$MinApprovedReviewRules"
} else {
Add-Check -Key "review_rule_signoff" -Label "review rule signoff" -Status "blocked" -Detail "approved_review_rules=$($gateMetrics.approved_review_rules); min=$MinApprovedReviewRules"
}
if ($gateMetrics.approved_review_rules_missing_evidence -eq 0) {
Add-Check -Key "review_rule_evidence" -Label "review rule evidence" -Status "pass" -Detail "approved review rules have evidence"
} else {
Add-Check -Key "review_rule_evidence" -Label "review rule evidence" -Status "blocked" -Detail "approved_review_rules_missing_evidence=$($gateMetrics.approved_review_rules_missing_evidence)"
}
if ($gateMetrics.approved_clauses -ge $MinApprovedClauses) {
Add-Check -Key "clause_signoff" -Label "clause signoff" -Status "pass" -Detail "approved_clauses=$($gateMetrics.approved_clauses); min=$MinApprovedClauses"
} else {
Add-Check -Key "clause_signoff" -Label "clause signoff" -Status "blocked" -Detail "approved_clauses=$($gateMetrics.approved_clauses); min=$MinApprovedClauses"
}
if ($gateMetrics.approved_clauses_missing_evidence -eq 0) {
Add-Check -Key "clause_evidence" -Label "clause evidence" -Status "pass" -Detail "approved clauses have evidence"
} else {
Add-Check -Key "clause_evidence" -Label "clause evidence" -Status "blocked" -Detail "approved_clauses_missing_evidence=$($gateMetrics.approved_clauses_missing_evidence)"
}
$checks |
ForEach-Object {
[pscustomobject]@{
key = $_.key
label = $_.label
required = $_.required
status = $_.status
detail = $_.detail
}
} |
Export-Csv -LiteralPath $checksCsv -NoTypeInformation -Encoding UTF8
$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"
}
$summary = [ordered]@{
status = $overallStatus
generated_at = (Get-Date).ToString("o")
repo_root = $repoRoot
source_mode = $dataSource
metrics = [ordered]@{
review_tasks = $gateMetrics.review_tasks
required_review_tasks = $gateMetrics.required_review_tasks
missing_reviewer_tasks = $gateMetrics.missing_reviewer_tasks
approved_review_tasks = $gateMetrics.approved_review_tasks
approved_review_tasks_missing_evidence = $gateMetrics.approved_review_tasks_missing_evidence
material_signoffs = $gateMetrics.material_signoffs
approved_materials = $gateMetrics.approved_materials
approved_materials_missing_evidence = $gateMetrics.approved_materials_missing_evidence
approved_public_materials = $gateMetrics.approved_public_materials
approved_public_materials_missing_evidence = $gateMetrics.approved_public_materials_missing_evidence
template_signoffs = $gateMetrics.template_signoffs
approved_templates = $gateMetrics.approved_templates
approved_templates_missing_evidence = $gateMetrics.approved_templates_missing_evidence
knowledge_signoffs = $gateMetrics.knowledge_signoffs
approved_knowledge_cards = $gateMetrics.approved_knowledge_cards
approved_knowledge_cards_missing_evidence = $gateMetrics.approved_knowledge_cards_missing_evidence
approved_review_rules = $gateMetrics.approved_review_rules
approved_review_rules_missing_evidence = $gateMetrics.approved_review_rules_missing_evidence
approved_clauses = $gateMetrics.approved_clauses
approved_clauses_missing_evidence = $gateMetrics.approved_clauses_missing_evidence
failed_required = $failedRequired
blocked_required = $blockedRequired
warnings = $warnings
}
checks = $checks.ToArray()
artifacts = [ordered]@{
checks = $checksCsv
summary_json = $summaryJson
summary_md = $summaryMd
}
inputs = [ordered]@{
review_task_package = $reviewTaskPath
material_signoff_register = $materialSignoffPath
template_signoff_register = $templateSignoffPath
knowledge_signoff_register = $knowledgeSignoffPath
database_snapshot = if ($null -ne $databaseSnapshot) { $databaseSnapshot.artifact } else { $null }
}
}
$summary | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $summaryJson -Encoding UTF8
$md = New-Object System.Collections.Generic.List[string]
$md.Add("# Formal Launch Approval Gate Summary") | Out-Null
$md.Add("") | Out-Null
$md.Add("- status: $overallStatus") | Out-Null
$md.Add("- generated_at: $($summary.generated_at)") | Out-Null
$md.Add("- repo_root: $repoRoot") | Out-Null
$md.Add("- source_mode: $dataSource") | 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("## Checks") | Out-Null
$md.Add("") | Out-Null
$md.Add("| check | required | status | detail |") | Out-Null
$md.Add("|---|---|---|---|") | Out-Null
foreach ($check in $checks) {
$detail = ([string]$check.detail).Replace("|", "/")
$md.Add("| $($check.label) | $($check.required) | $($check.status.ToUpperInvariant()) | $detail |") | Out-Null
}
$md.Add("") | Out-Null
$md.Add("PASS means the PC formal-launch candidate has the required preparation gate, real reviewer assignments, completed review evidence, and production signoff registers. BLOCKED is expected until real reviewers complete and sign the required assets.") | Out-Null
Write-Utf8File -Path $summaryMd -Text ($md -join [Environment]::NewLine)
Write-Output "FORMAL_LAUNCH_APPROVAL_GATE_$overallStatus $ArtifactRoot"
if ($overallStatus -eq "FAIL") {
exit 1
}
if ($overallStatus -eq "BLOCKED" -and $FailOnBlocked) {
throw "formal launch approval gate blocked: $summaryMd"
}