param( [string]$ArtifactRoot = "", [switch]$SkipBuild ) $ErrorActionPreference = "Stop" $repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..") $frontendDir = Join-Path $repoRoot "frontend" $timestamp = Get-Date -Format "yyyy-MM-ddTHH-mm-ss" if ([string]::IsNullOrWhiteSpace($ArtifactRoot)) { $ArtifactRoot = Join-Path $repoRoot "project-docs\qa-artifacts\phase5-dependency-governance" } if (-not [System.IO.Path]::IsPathRooted($ArtifactRoot)) { $ArtifactRoot = Join-Path $repoRoot $ArtifactRoot } $artifactDir = Join-Path $ArtifactRoot $timestamp $summaryJson = Join-Path $artifactDir "dependency-governance-summary.json" $summaryMd = Join-Path $artifactDir "dependency-governance-summary.md" New-Item -ItemType Directory -Force -Path $artifactDir | Out-Null if (-not (Test-Path "$HOME\.Codex\skills\gstack\bin")) { throw "gstack is required for all AI-assisted work in this repo." } $checks = New-Object System.Collections.Generic.List[object] function Add-Check { param( [string]$Key, [string]$Label, [ValidateSet("pass", "fail", "warn")] [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 12 | Set-Content -Encoding UTF8 -Path $Path } function Invoke-CommandWithLog { param( [string]$Name, [string]$Command, [string]$WorkingDirectory ) $logPath = Join-Path $artifactDir "$Name.log" $previousErrorActionPreference = $ErrorActionPreference $ErrorActionPreference = "Continue" Push-Location $WorkingDirectory try { $output = & powershell.exe -NoProfile -ExecutionPolicy Bypass -Command $Command 2>&1 $exitCode = $LASTEXITCODE } finally { Pop-Location $ErrorActionPreference = $previousErrorActionPreference } ($output -join [Environment]::NewLine) | Set-Content -Encoding UTF8 -Path $logPath return [ordered]@{ exit_code = $exitCode log = $logPath } } $auditPath = Join-Path $artifactDir "npm-audit.json" Push-Location $frontendDir try { $previousErrorActionPreference = $ErrorActionPreference $ErrorActionPreference = "Continue" try { $auditOutput = npm.cmd audit --json 2>&1 $auditExit = $LASTEXITCODE } finally { $ErrorActionPreference = $previousErrorActionPreference } ($auditOutput -join [Environment]::NewLine) | Set-Content -Encoding UTF8 -Path $auditPath $auditJson = Get-Content -Raw -Encoding UTF8 $auditPath | ConvertFrom-Json $auditTotals = $auditJson.metadata.vulnerabilities $totalVulnerabilities = [int]$auditTotals.total $highCritical = [int]$auditTotals.high + [int]$auditTotals.critical if ($auditExit -eq 0 -and $totalVulnerabilities -eq 0) { Add-Check -Key "npm_audit" -Label "npm audit" -Status "pass" -Detail "npm audit reports 0 vulnerabilities" -Meta @{ artifact = $auditPath } } elseif ($highCritical -eq 0) { Add-Check -Key "npm_audit" -Label "npm audit" -Status "warn" -Detail "npm audit has no high/critical vulnerabilities but still reports lower risks" -Meta @{ total = $totalVulnerabilities moderate = [int]$auditTotals.moderate artifact = $auditPath } } else { Add-Check -Key "npm_audit" -Label "npm audit" -Status "fail" -Detail "npm audit reports high/critical vulnerabilities" -Meta @{ high = [int]$auditTotals.high critical = [int]$auditTotals.critical artifact = $auditPath } } $npmLs = Invoke-CommandWithLog -Name "npm-ls-next-postcss" -Command "npm.cmd ls next postcss eslint-config-next" -WorkingDirectory $frontendDir if ($npmLs.exit_code -eq 0) { Add-Check -Key "npm_dependency_tree" -Label "npm dependency tree" -Status "pass" -Detail "next/postcss/eslint-config-next tree resolved" -Meta @{ log = $npmLs.log } } else { Add-Check -Key "npm_dependency_tree" -Label "npm dependency tree" -Status "fail" -Detail "npm dependency tree command failed" -Meta @{ log = $npmLs.log } } $lint = Invoke-CommandWithLog -Name "frontend-lint" -Command "npm.cmd run lint" -WorkingDirectory $frontendDir if ($lint.exit_code -eq 0) { Add-Check -Key "frontend_lint" -Label "frontend lint" -Status "pass" -Detail "npm run lint passed" -Meta @{ log = $lint.log } } else { Add-Check -Key "frontend_lint" -Label "frontend lint" -Status "fail" -Detail "npm run lint failed" -Meta @{ log = $lint.log } } if ($SkipBuild) { Add-Check -Key "frontend_build" -Label "frontend build" -Status "warn" -Required $false -Detail "Skipped by -SkipBuild" } else { $build = Invoke-CommandWithLog -Name "frontend-build" -Command "npm.cmd run build" -WorkingDirectory $frontendDir if ($build.exit_code -eq 0) { Add-Check -Key "frontend_build" -Label "frontend build" -Status "pass" -Detail "npm run build passed" -Meta @{ log = $build.log } } else { Add-Check -Key "frontend_build" -Label "frontend build" -Status "fail" -Detail "npm run build failed" -Meta @{ log = $build.log } } } } finally { Pop-Location } $pipCheck = Invoke-CommandWithLog -Name "backend-pip-check" -Command ".\backend\.venv\Scripts\python.exe -m pip check" -WorkingDirectory $repoRoot if ($pipCheck.exit_code -eq 0) { Add-Check -Key "backend_pip_check" -Label "backend pip check" -Status "pass" -Detail "pip check passed" -Meta @{ log = $pipCheck.log } } else { Add-Check -Key "backend_pip_check" -Label "backend pip check" -Status "fail" -Detail "pip check failed" -Meta @{ log = $pipCheck.log } } $failedRequired = @($checks | Where-Object { $_.required -and $_.status -eq "fail" }) $warningsList = @($checks | Where-Object { $_.status -eq "warn" }) $overallStatus = if ($failedRequired.Count -gt 0) { "FAIL" } elseif ($warningsList.Count -gt 0) { "WARN" } else { "PASS" } $summary = [ordered]@{ name = "phase5-dependency-governance" status = $overallStatus generated_at = (Get-Date).ToUniversalTime().ToString("o") artifact_dir = $artifactDir checks = $checks totals = [ordered]@{ total = $checks.Count failed_required = $failedRequired.Count warnings = $warningsList.Count } } Save-Json -Path $summaryJson -Value $summary $markdown = New-Object System.Collections.Generic.List[string] $markdown.Add("# Phase 5 Dependency Governance") | Out-Null $markdown.Add("") | Out-Null $markdown.Add("- Status: $overallStatus") | Out-Null $markdown.Add("- Generated at: $($summary.generated_at)") | Out-Null $markdown.Add("- Artifact dir: $artifactDir") | 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('- `dependency-governance-summary.json`') | Out-Null $markdown.Add('- `npm-audit.json`') | Out-Null $markdown.Add('- `*.log`') | Out-Null $markdown | Set-Content -Encoding UTF8 -Path $summaryMd Write-Host "PHASE5_DEPENDENCY_GOVERNANCE_$overallStatus $artifactDir" if ($overallStatus -eq "FAIL") { throw "Phase 5 dependency governance failed. See $summaryMd" }