param( [string]$ArtifactRoot = "", [string]$FixtureRoot = "", [string]$Phase8ArtifactRoot = "", [string]$Phase9ArtifactRoot = "", [string]$Phase10ArtifactRoot = "", [string]$Phase11ArtifactRoot = "", [int]$MaxEvidenceAgeHours = 96, [switch]$FailOnWarn, [switch]$FailOnExternalBlocked ) $ErrorActionPreference = "Stop" $repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path $timestamp = Get-Date -Format "yyyy-MM-ddTHH-mm-ss-fff" if ([string]::IsNullOrWhiteSpace($ArtifactRoot)) { $ArtifactRoot = Join-Path $repoRoot "project-docs\qa-artifacts\phase12-v2-roadmap-gate" } if ([string]::IsNullOrWhiteSpace($FixtureRoot)) { $FixtureRoot = Join-Path $repoRoot "project-docs\phase12-v2-roadmap-fixtures" } if ([string]::IsNullOrWhiteSpace($Phase8ArtifactRoot)) { $Phase8ArtifactRoot = Join-Path $repoRoot "project-docs\qa-artifacts\phase8-pilot-closed-loop" } if ([string]::IsNullOrWhiteSpace($Phase9ArtifactRoot)) { $Phase9ArtifactRoot = Join-Path $repoRoot "project-docs\qa-artifacts\phase9-business-quality-gate" } if ([string]::IsNullOrWhiteSpace($Phase10ArtifactRoot)) { $Phase10ArtifactRoot = Join-Path $repoRoot "project-docs\qa-artifacts\phase10-delivery-acceptance" } if ([string]::IsNullOrWhiteSpace($Phase11ArtifactRoot)) { $Phase11ArtifactRoot = Join-Path $repoRoot "project-docs\qa-artifacts\phase11-scale-ops-gate" } foreach ($pathName in @("ArtifactRoot", "FixtureRoot", "Phase8ArtifactRoot", "Phase9ArtifactRoot", "Phase10ArtifactRoot", "Phase11ArtifactRoot")) { $value = Get-Variable -Name $pathName -ValueOnly if (-not [System.IO.Path]::IsPathRooted($value)) { Set-Variable -Name $pathName -Value (Join-Path $repoRoot $value) } } $artifactDir = Join-Path $ArtifactRoot $timestamp New-Item -ItemType Directory -Force -Path $artifactDir | Out-Null $summaryJson = Join-Path $artifactDir "v2-roadmap-gate-summary.json" $summaryMd = Join-Path $artifactDir "v2-roadmap-gate-summary.md" $gitStatusPath = Join-Path $artifactDir "git-status.txt" $evidenceMatrixCsv = Join-Path $artifactDir "phase12-evidence-matrix.csv" $demandReportCsv = Join-Path $artifactDir "phase12-demand-pool-report.csv" $firstBatchCsv = Join-Path $artifactDir "phase12-first-batch-report.csv" $roadmapReportCsv = Join-Path $artifactDir "phase12-roadmap-report.csv" $riskReportCsv = Join-Path $artifactDir "phase12-risk-and-spike-report.csv" $checks = New-Object System.Collections.Generic.List[object] $evidenceMatrix = New-Object System.Collections.Generic.List[object] 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 Save-Json { param([string]$Path, [object]$Value) $Value | ConvertTo-Json -Depth 18 | Set-Content -Encoding UTF8 -Path $Path } function Read-JsonFile { param([string]$Path) if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return $null } try { return Get-Content -LiteralPath $Path -Raw -Encoding UTF8 | ConvertFrom-Json } catch { return $null } } function Get-LatestSummaryArtifact { param( [string]$Root, [string]$SummaryFile ) if (-not (Test-Path -LiteralPath $Root -PathType Container)) { return $null } return Get-ChildItem -LiteralPath $Root -Directory | Sort-Object LastWriteTime -Descending | Where-Object { Test-Path -LiteralPath (Join-Path $_.FullName $SummaryFile) } | Select-Object -First 1 } function Test-CsvColumns { param( [object[]]$Rows, [string[]]$Columns ) if ($Rows.Count -eq 0) { return $false } $names = @($Rows[0].PSObject.Properties.Name) foreach ($column in $Columns) { if ($names -notcontains $column) { return $false } } return $true } function Test-RequiredFlag { param([string]$Value) return @("true", "yes", "1", "required") -contains $Value.ToLowerInvariant() } function Get-FailedRequiredCount { param([object]$Summary) if ($null -eq $Summary -or $null -eq $Summary.totals) { return 0 } if ($null -ne $Summary.totals.failed_required) { return [int]$Summary.totals.failed_required } return 0 } function Add-EvidenceCheck { param( [string]$Key, [string]$Label, [string]$Root, [string]$SummaryFile, [string[]]$AllowedStatuses ) $latest = Get-LatestSummaryArtifact -Root $Root -SummaryFile $SummaryFile if ($null -eq $latest) { Add-Check -Key $Key -Label $Label -Status "fail" -Detail "No evidence artifact found" $evidenceMatrix.Add([pscustomobject][ordered]@{ key = $Key label = $Label status = "MISSING" failed_required = "" age_hours = "" artifact_dir = "" summary_file = "" }) | Out-Null return $null } $summaryPath = Join-Path $latest.FullName $SummaryFile $summary = Read-JsonFile -Path $summaryPath $status = if ($summary -and $summary.status) { [string]$summary.status } else { "UNKNOWN" } $failedRequired = Get-FailedRequiredCount -Summary $summary $ageHours = [math]::Round(((Get-Date) - $latest.LastWriteTime).TotalHours, 2) $evidenceMatrix.Add([pscustomobject][ordered]@{ key = $Key label = $Label status = $status failed_required = $failedRequired age_hours = $ageHours artifact_dir = $latest.FullName summary_file = $summaryPath }) | Out-Null if (($AllowedStatuses -contains $status) -and $failedRequired -eq 0) { Add-Check -Key $Key -Label $Label -Status "pass" -Detail "Status=$status, failed_required=$failedRequired, age=${ageHours}h" -Meta @{ artifact_dir = $latest.FullName; summary = $summaryPath } } else { Add-Check -Key $Key -Label $Label -Status "fail" -Detail "Status=$status, failed_required=$failedRequired" -Meta @{ artifact_dir = $latest.FullName; summary = $summaryPath } } if ($ageHours -gt $MaxEvidenceAgeHours) { Add-Check -Key ($Key + "_freshness") -Label ($Label + " freshness") -Status "warn" -Required $false -Detail "Evidence age ${ageHours}h exceeds ${MaxEvidenceAgeHours}h" -Meta @{ artifact_dir = $latest.FullName } } return $summary } if (-not (Test-Path "$HOME\.Codex\skills\gstack\bin")) { Add-Check -Key "gstack" -Label "gstack preflight" -Status "fail" -Detail "gstack is missing" } else { Add-Check -Key "gstack" -Label "gstack preflight" -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 remote" -Status "pass" -Detail "origin points to 192.168.3.90" -Meta @{ branch = $branch; commit = $commit; remote = $remoteUrl } } else { Add-Check -Key "origin_remote" -Label "origin remote" -Status "fail" -Detail "origin does not point to 192.168.3.90" -Meta @{ branch = $branch; commit = $commit; remote = $remoteUrl } } if ($gitStatus) { Add-Check -Key "working_tree" -Label "working tree" -Status "warn" -Required $false -Detail "Working tree has uncommitted changes; V2 roadmap gate is advisory" -Meta @{ artifact = $gitStatusPath } } else { Add-Check -Key "working_tree" -Label "working tree" -Status "pass" -Detail "Working tree is clean" -Meta @{ artifact = $gitStatusPath } } if (Test-Path -LiteralPath $FixtureRoot -PathType Container) { Add-Check -Key "fixture_root" -Label "V2 roadmap fixture root" -Status "pass" -Detail "Fixture root exists" -Meta @{ root = $FixtureRoot } } else { Add-Check -Key "fixture_root" -Label "V2 roadmap fixture root" -Status "fail" -Detail "Fixture root is missing" -Meta @{ root = $FixtureRoot } } $requiredFixtureFiles = @( "README.md", "v2-demand-pool.csv", "v2-priority-scorecard.csv", "v2-first-batch-backlog.csv", "v2-roadmap.csv", "v2-scope-decision-register.csv", "integration-expansion-plan.csv", "technical-spike-register.csv", "v2-acceptance-criteria.csv", "long-term-roadmap.md" ) foreach ($file in $requiredFixtureFiles) { $path = Join-Path $FixtureRoot $file if (Test-Path -LiteralPath $path -PathType Leaf) { Add-Check -Key ("fixture_" + ($file -replace '[^a-zA-Z0-9]', '_')) -Label "fixture $file" -Status "pass" -Detail "Fixture exists" -Meta @{ path = $path } } else { Add-Check -Key ("fixture_" + ($file -replace '[^a-zA-Z0-9]', '_')) -Label "fixture $file" -Status "fail" -Detail "Fixture missing" -Meta @{ path = $path } } } $demandRows = @(Import-Csv -LiteralPath (Join-Path $FixtureRoot "v2-demand-pool.csv")) $scoreRows = @(Import-Csv -LiteralPath (Join-Path $FixtureRoot "v2-priority-scorecard.csv")) $firstBatchRows = @(Import-Csv -LiteralPath (Join-Path $FixtureRoot "v2-first-batch-backlog.csv")) $roadmapRows = @(Import-Csv -LiteralPath (Join-Path $FixtureRoot "v2-roadmap.csv")) $decisionRows = @(Import-Csv -LiteralPath (Join-Path $FixtureRoot "v2-scope-decision-register.csv")) $integrationRows = @(Import-Csv -LiteralPath (Join-Path $FixtureRoot "integration-expansion-plan.csv")) $spikeRows = @(Import-Csv -LiteralPath (Join-Path $FixtureRoot "technical-spike-register.csv")) $acceptanceRows = @(Import-Csv -LiteralPath (Join-Path $FixtureRoot "v2-acceptance-criteria.csv")) $phase8Summary = Add-EvidenceCheck -Key "phase8_closed_loop" -Label "phase8 closed loop" -Root $Phase8ArtifactRoot -SummaryFile "phase8-pilot-closed-loop-summary.json" -AllowedStatuses @("PASS", "PASS_WITH_WEBHOOK_BLOCKED") $phase9Summary = Add-EvidenceCheck -Key "phase9_business_quality" -Label "phase9 business quality" -Root $Phase9ArtifactRoot -SummaryFile "business-quality-gate-summary.json" -AllowedStatuses @("PASS", "PASS_WITH_WEBHOOK_BLOCKED") $phase10Summary = Add-EvidenceCheck -Key "phase10_delivery_acceptance" -Label "phase10 delivery acceptance" -Root $Phase10ArtifactRoot -SummaryFile "delivery-acceptance-summary.json" -AllowedStatuses @("PASS", "PASS_WITH_WEBHOOK_BLOCKED") $phase11Summary = Add-EvidenceCheck -Key "phase11_scale_ops" -Label "phase11 scale ops" -Root $Phase11ArtifactRoot -SummaryFile "scale-ops-gate-summary.json" -AllowedStatuses @("PASS", "PASS_WITH_WEBHOOK_BLOCKED") $demandColumnsOk = Test-CsvColumns -Rows $demandRows -Columns @("demand_id", "source_phase", "source_evidence", "theme", "description", "value_score", "risk_score", "dependency_score", "cost_score", "priority_score", "decision", "status", "acceptance_criteria", "regression_gate") $sourcePhases = @($demandRows | Select-Object -ExpandProperty source_phase -Unique) $requiredSourcePhases = @("phase8", "phase9", "phase10", "phase11") $missingSourcePhases = @($requiredSourcePhases | Where-Object { $sourcePhases -notcontains $_ }) $badDemandScores = @($demandRows | Where-Object { [int]$_.priority_score -lt 0 -or [int]$_.value_score -lt 1 -or [string]::IsNullOrWhiteSpace($_.source_evidence) }) if ($demandColumnsOk -and $demandRows.Count -ge 10 -and $missingSourcePhases.Count -eq 0 -and $badDemandScores.Count -eq 0) { Add-Check -Key "demand_pool" -Label "V2 demand pool" -Status "pass" -Detail "Demands=$($demandRows.Count), phases=$($sourcePhases -join ',')" } else { Add-Check -Key "demand_pool" -Label "V2 demand pool" -Status "fail" -Detail "Demand pool incomplete or missing phase evidence" -Meta @{ missing_source_phases = $missingSourcePhases } } $scoreColumnsOk = Test-CsvColumns -Rows $scoreRows -Columns @("demand_id", "value_score", "risk_score", "dependency_score", "cost_score", "base_score", "strategic_boost_score", "priority_score", "priority_band", "rationale") $scoreIds = @($scoreRows | Select-Object -ExpandProperty demand_id) $missingScores = @($demandRows | Where-Object { $scoreIds -notcontains $_.demand_id } | Select-Object -ExpandProperty demand_id) $scorePriorityByDemand = @{} foreach ($scoreRow in $scoreRows) { $scorePriorityByDemand[$scoreRow.demand_id] = [int]$scoreRow.priority_score } $demandPriorityMismatch = @($demandRows | Where-Object { $scorePriorityByDemand.ContainsKey($_.demand_id) -and [int]$_.priority_score -ne $scorePriorityByDemand[$_.demand_id] } | Select-Object -ExpandProperty demand_id) $badScoreMath = @($scoreRows | Where-Object { $baseScore = [int]$_.value_score + 10 - [int]$_.risk_score - [int]$_.dependency_score - [int]$_.cost_score $declaredBaseScore = [int]$_.base_score $strategicBoostScore = [int]$_.strategic_boost_score $priorityScore = [int]$_.priority_score $baseScore -ne $declaredBaseScore -or $strategicBoostScore -lt 0 -or $strategicBoostScore -gt 3 -or ($baseScore + $strategicBoostScore) -ne $priorityScore }) if ($scoreColumnsOk -and $missingScores.Count -eq 0 -and $badScoreMath.Count -eq 0 -and $demandPriorityMismatch.Count -eq 0) { Add-Check -Key "priority_scorecard" -Label "priority scorecard" -Status "pass" -Detail "Score rows=$($scoreRows.Count), formula=base+strategic_boost" } else { Add-Check -Key "priority_scorecard" -Label "priority scorecard" -Status "fail" -Detail "Priority scorecard incomplete or score math invalid" -Meta @{ missing_scores = $missingScores; bad_score_math = @($badScoreMath | Select-Object -ExpandProperty demand_id); demand_priority_mismatch = $demandPriorityMismatch } } $acceptanceIds = @($acceptanceRows | Select-Object -ExpandProperty acceptance_id) $firstBatchColumnsOk = Test-CsvColumns -Rows $firstBatchRows -Columns @("backlog_id", "demand_id", "epic", "story", "owner", "status", "acceptance_ref", "regression_gate", "ready_for_dev") $readyFirstBatch = @($firstBatchRows | Where-Object { $_.status -eq "READY" -and (Test-RequiredFlag $_.ready_for_dev) }) $firstBatchMissingAcceptance = @($firstBatchRows | Where-Object { $acceptanceIds -notcontains $_.acceptance_ref }) if ($firstBatchColumnsOk -and $readyFirstBatch.Count -ge 5 -and $firstBatchMissingAcceptance.Count -eq 0) { Add-Check -Key "first_batch" -Label "V2 first batch" -Status "pass" -Detail "Ready first batch items=$($readyFirstBatch.Count)" } else { Add-Check -Key "first_batch" -Label "V2 first batch" -Status "fail" -Detail "First batch is not ready or lacks acceptance references" } $roadmapColumnsOk = Test-CsvColumns -Rows $roadmapRows -Columns @("roadmap_id", "horizon", "demand_id", "theme", "decision", "status", "target_phase", "entry_criteria", "exit_criteria") $frozenRows = @($roadmapRows | Where-Object { $_.status -eq "FROZEN" }) $nowRows = @($roadmapRows | Where-Object { $_.horizon -eq "now" }) $missingNowDemand = @($readyFirstBatch | Where-Object { @($nowRows | Select-Object -ExpandProperty demand_id) -notcontains $_.demand_id }) if ($roadmapColumnsOk -and $frozenRows.Count -eq $roadmapRows.Count -and $nowRows.Count -ge 5 -and $missingNowDemand.Count -eq 0) { Add-Check -Key "roadmap_frozen" -Label "V2 roadmap frozen" -Status "pass" -Detail "Roadmap rows=$($roadmapRows.Count), now=$($nowRows.Count)" } else { Add-Check -Key "roadmap_frozen" -Label "V2 roadmap frozen" -Status "fail" -Detail "Roadmap is not frozen or first batch is not in now horizon" } $decisionColumnsOk = Test-CsvColumns -Rows $decisionRows -Columns @("decision_id", "demand_id", "decision", "reason", "owner", "status") $approvedDecisionIds = @($decisionRows | Where-Object { $_.status -eq "APPROVED" } | Select-Object -ExpandProperty demand_id) $missingDecisions = @($demandRows | Where-Object { $approvedDecisionIds -notcontains $_.demand_id } | Select-Object -ExpandProperty demand_id) if ($decisionColumnsOk -and $missingDecisions.Count -eq 0) { Add-Check -Key "scope_decisions" -Label "scope decisions" -Status "pass" -Detail "Approved decisions=$($decisionRows.Count)" } else { Add-Check -Key "scope_decisions" -Label "scope decisions" -Status "fail" -Detail "Scope decisions missing approval" } $integrationColumnsOk = Test-CsvColumns -Rows $integrationRows -Columns @("integration_id", "demand_id", "integration", "priority", "status", "dependency", "security_boundary", "phase") $p1Integrations = @($integrationRows | Where-Object { $_.priority -eq "P1" }) $blockedIntegrations = @($integrationRows | Where-Object { $_.status -like "BLOCKED*" }) if ($integrationColumnsOk -and $integrationRows.Count -ge 5 -and $p1Integrations.Count -ge 3) { Add-Check -Key "integration_expansion" -Label "integration expansion" -Status "pass" -Detail "Integrations=$($integrationRows.Count), P1=$($p1Integrations.Count)" } else { Add-Check -Key "integration_expansion" -Label "integration expansion" -Status "fail" -Detail "Integration expansion plan incomplete" } $spikeColumnsOk = Test-CsvColumns -Rows $spikeRows -Columns @("spike_id", "demand_id", "risk_level", "status", "objective", "exit_criteria", "owner") $spikeDemandIds = @($spikeRows | Select-Object -ExpandProperty demand_id) $highRiskDemandIds = @($demandRows | Where-Object { [int]$_.risk_score -ge 5 } | Select-Object -ExpandProperty demand_id) $highRiskWithoutSpike = @($highRiskDemandIds | Where-Object { $spikeDemandIds -notcontains $_ }) if ($spikeColumnsOk -and $highRiskWithoutSpike.Count -eq 0) { Add-Check -Key "technical_spikes" -Label "technical spikes" -Status "pass" -Detail "High-risk demands covered=$($highRiskDemandIds.Count)" } else { Add-Check -Key "technical_spikes" -Label "technical spikes" -Status "fail" -Detail "High-risk demands missing spike coverage" -Meta @{ missing = $highRiskWithoutSpike } } $acceptanceColumnsOk = Test-CsvColumns -Rows $acceptanceRows -Columns @("acceptance_id", "demand_id", "criteria", "verification_gate", "regression_scope", "status", "required") $requiredAcceptanceBad = @($acceptanceRows | Where-Object { (Test-RequiredFlag $_.required) -and $_.status -ne "READY" }) if ($acceptanceColumnsOk -and $requiredAcceptanceBad.Count -eq 0 -and $acceptanceRows.Count -ge $firstBatchRows.Count) { Add-Check -Key "acceptance_criteria" -Label "acceptance criteria" -Status "pass" -Detail "Acceptance criteria=$($acceptanceRows.Count)" } else { Add-Check -Key "acceptance_criteria" -Label "acceptance criteria" -Status "fail" -Detail "Acceptance criteria incomplete" } $longTermRoadmap = Get-Content -LiteralPath (Join-Path $FixtureRoot "long-term-roadmap.md") -Raw -Encoding UTF8 if ($longTermRoadmap.Contains("V2.1") -and $longTermRoadmap.Contains("V2.2") -and $longTermRoadmap.Contains("V2.3") -and $longTermRoadmap.Contains("Guardrail")) { Add-Check -Key "long_term_roadmap" -Label "long-term roadmap" -Status "pass" -Detail "Long-term roadmap covers V2.1, V2.2, V2.3 and guardrail" } else { Add-Check -Key "long_term_roadmap" -Label "long-term roadmap" -Status "fail" -Detail "Long-term roadmap narrative incomplete" } $demandRows | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $demandReportCsv $firstBatchRows | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $firstBatchCsv $roadmapRows | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $roadmapReportCsv $spikeRows | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $riskReportCsv $evidenceMatrix | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $evidenceMatrixCsv $secret = [string]::Concat([char]54, [char]53, [char]52, [char]51, [char]50, [char]49) $secretHits = @() $scanFiles = @(Get-ChildItem -LiteralPath $FixtureRoot -File -Recurse) $scanFiles += @(Get-Item -LiteralPath (Join-Path $repoRoot "scripts\run-v2-roadmap-gate.ps1")) foreach ($file in $scanFiles) { $text = Get-Content -LiteralPath $file.FullName -Raw -Encoding UTF8 -ErrorAction SilentlyContinue if ($text -and $text.Contains($secret)) { $secretHits += $file.FullName } } if ($secretHits.Count -eq 0) { Add-Check -Key "secret_scan" -Label "secret scan" -Status "pass" -Detail "No administrator password literal found in V2 roadmap package or gate script" } else { Add-Check -Key "secret_scan" -Label "secret scan" -Status "fail" -Detail "Secret literal detected" -Meta @{ hits = $secretHits } } $externalBlocked = $false if (($phase8Summary -and $phase8Summary.status -eq "PASS_WITH_WEBHOOK_BLOCKED") -or ($phase9Summary -and $phase9Summary.status -eq "PASS_WITH_WEBHOOK_BLOCKED") -or ($phase10Summary -and $phase10Summary.status -eq "PASS_WITH_WEBHOOK_BLOCKED") -or ($phase11Summary -and $phase11Summary.status -eq "PASS_WITH_WEBHOOK_BLOCKED") -or $blockedIntegrations.Count -gt 0) { $externalBlocked = $true Add-Check -Key "external_webhook" -Label "external alert webhook" -Status "blocked" -Required $false -Detail "Real external alert Webhook remains unavailable; inherited into V2 roadmap" } $failedRequired = @($checks | Where-Object { $_.required -and $_.status -eq "fail" }) $warningsList = @($checks | Where-Object { $_.status -eq "warn" }) $blockedList = @($checks | Where-Object { $_.status -eq "blocked" }) $overallStatus = if ($failedRequired.Count -gt 0) { "FAIL" } elseif ($blockedList.Count -gt 0 -or $externalBlocked) { "PASS_WITH_WEBHOOK_BLOCKED" } elseif ($warningsList.Count -gt 0) { "WARN" } else { "PASS" } $alertSeverity = if ($overallStatus -eq "FAIL") { "critical" } elseif ($overallStatus -eq "WARN") { "warning" } elseif ($overallStatus -eq "PASS_WITH_WEBHOOK_BLOCKED") { "blocked" } else { "ok" } $summary = [ordered]@{ name = "phase12-v2-roadmap-gate" status = $overallStatus alert_severity = $alertSeverity generated_at = (Get-Date).ToUniversalTime().ToString("o") artifact_dir = $artifactDir branch = $branch commit = $commit remote = $remoteUrl fixture_root = $FixtureRoot checks = $checks metrics = [ordered]@{ demands = $demandRows.Count first_batch_items = $firstBatchRows.Count roadmap_rows = $roadmapRows.Count approved_decisions = $decisionRows.Count integrations = $integrationRows.Count technical_spikes = $spikeRows.Count acceptance_criteria = $acceptanceRows.Count blocked_integrations = $blockedIntegrations.Count } artifacts = [ordered]@{ evidence_matrix = $evidenceMatrixCsv demand_pool_report = $demandReportCsv first_batch_report = $firstBatchCsv roadmap_report = $roadmapReportCsv risk_and_spike_report = $riskReportCsv } totals = [ordered]@{ total = $checks.Count failed_required = $failedRequired.Count warnings = $warningsList.Count blocked = $blockedList.Count } } Save-Json -Path $summaryJson -Value $summary $markdown = New-Object System.Collections.Generic.List[string] $markdown.Add("# Phase 12 V2 Roadmap Gate") | Out-Null $markdown.Add("") | Out-Null $markdown.Add("- Status: $overallStatus") | Out-Null $markdown.Add("- Alert severity: $alertSeverity") | Out-Null $markdown.Add("- Generated at: $($summary.generated_at)") | Out-Null $markdown.Add("- Branch: $branch") | Out-Null $markdown.Add("- Commit: $commit") | Out-Null $markdown.Add("- Artifact dir: $artifactDir") | Out-Null $markdown.Add("- Fixture root: $FixtureRoot") | Out-Null $markdown.Add("") | Out-Null $markdown.Add("## Metrics") | Out-Null $markdown.Add("") | Out-Null $markdown.Add("| Metric | Count |") | Out-Null $markdown.Add("|---|---:|") | Out-Null $markdown.Add("| Demands | $($demandRows.Count) |") | Out-Null $markdown.Add("| First batch items | $($firstBatchRows.Count) |") | Out-Null $markdown.Add("| Roadmap rows | $($roadmapRows.Count) |") | Out-Null $markdown.Add("| Approved decisions | $($decisionRows.Count) |") | Out-Null $markdown.Add("| Integrations | $($integrationRows.Count) |") | Out-Null $markdown.Add("| Technical spikes | $($spikeRows.Count) |") | Out-Null $markdown.Add("| Acceptance criteria | $($acceptanceRows.Count) |") | Out-Null $markdown.Add("| Blocked integrations | $($blockedIntegrations.Count) |") | Out-Null $markdown.Add("") | Out-Null $markdown.Add("## Checks") | Out-Null $markdown.Add("") | Out-Null $markdown.Add("| Check | Required | Status | Detail |") | Out-Null $markdown.Add("|---|---:|---|---|") | Out-Null foreach ($check in $checks) { $markdown.Add("| $($check.label) | $($check.required) | $($check.status.ToUpperInvariant()) | $($check.detail) |") | Out-Null } $markdown.Add("") | Out-Null $markdown.Add("Local artifacts:") | Out-Null $markdown.Add("") | Out-Null $markdown.Add('- `v2-roadmap-gate-summary.json`') | Out-Null $markdown.Add('- `v2-roadmap-gate-summary.md`') | Out-Null $markdown.Add('- `phase12-evidence-matrix.csv`') | Out-Null $markdown.Add('- `phase12-demand-pool-report.csv`') | Out-Null $markdown.Add('- `phase12-first-batch-report.csv`') | Out-Null $markdown.Add('- `phase12-roadmap-report.csv`') | Out-Null $markdown.Add('- `phase12-risk-and-spike-report.csv`') | Out-Null $markdown | Set-Content -Encoding UTF8 -Path $summaryMd Write-Host "PHASE12_V2_ROADMAP_GATE_$overallStatus $artifactDir" if ($overallStatus -eq "FAIL" -or ($FailOnWarn -and $warningsList.Count -gt 0) -or ($FailOnExternalBlocked -and $blockedList.Count -gt 0)) { throw "Phase 12 V2 roadmap gate $overallStatus. See $summaryMd" }