Files
ai-contract-platform/scripts/run-business-quality-gate.ps1
T
2026-05-01 18:46:35 +08:00

514 lines
23 KiB
PowerShell

param(
[string]$ArtifactRoot = "",
[string]$FixtureRoot = "",
[string]$Phase8ArtifactRoot = "",
[int]$MaxPhase8AgeHours = 48,
[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\phase9-business-quality-gate"
}
if ([string]::IsNullOrWhiteSpace($FixtureRoot)) {
$FixtureRoot = Join-Path $repoRoot "project-docs\phase9-quality-fixtures"
}
if ([string]::IsNullOrWhiteSpace($Phase8ArtifactRoot)) {
$Phase8ArtifactRoot = Join-Path $repoRoot "project-docs\qa-artifacts\phase8-pilot-closed-loop"
}
foreach ($pathName in @("ArtifactRoot", "FixtureRoot", "Phase8ArtifactRoot")) {
$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 "business-quality-gate-summary.json"
$summaryMd = Join-Path $artifactDir "business-quality-gate-summary.md"
$scorecardCsv = Join-Path $artifactDir "phase9-quality-scorecard.csv"
$defectCsv = Join-Path $artifactDir "phase9-quality-defect-register.csv"
$gitStatusPath = Join-Path $artifactDir "git-status.txt"
$checks = 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 Get-Scenario {
param(
[object[]]$Rows,
[string]$ScenarioId
)
return @($Rows | Where-Object { $_.scenario_id -eq $ScenarioId } | Select-Object -First 1)[0]
}
function Test-ScenariosPass {
param(
[object[]]$Rows,
[string[]]$ScenarioIds
)
foreach ($scenarioId in $ScenarioIds) {
$item = Get-Scenario -Rows $Rows -ScenarioId $scenarioId
if ($null -eq $item -or $item.status -ne "PASS") {
return $false
}
}
return $true
}
function Write-DomainReport {
param(
[string]$Domain,
[string]$Title,
[object[]]$FixtureRows,
[string[]]$ScenarioIds,
[object[]]$ScenarioRows,
[string[]]$RequiredColumns,
[string]$OutputPrefix,
[string]$ExtraDetail = ""
)
$columnsOk = Test-CsvColumns -Rows $FixtureRows -Columns $RequiredColumns
$fixturesReady = $FixtureRows.Count -gt 0 -and $columnsOk
$scenariosPass = Test-ScenariosPass -Rows $ScenarioRows -ScenarioIds $ScenarioIds
$score = if ($fixturesReady -and $scenariosPass) { 100 } elseif ($fixturesReady -or $scenariosPass) { 50 } else { 0 }
$status = if ($score -eq 100) { "PASS" } else { "FAIL" }
$jsonPath = Join-Path $artifactDir "$OutputPrefix.json"
$mdPath = Join-Path $artifactDir "$OutputPrefix.md"
$record = [ordered]@{
domain = $Domain
title = $Title
status = $status
score = $score
fixture_count = $FixtureRows.Count
required_columns = $RequiredColumns
scenario_ids = $ScenarioIds
scenarios_pass = $scenariosPass
fixtures_ready = $fixturesReady
extra_detail = $ExtraDetail
}
Save-Json -Path $jsonPath -Value $record
$markdown = New-Object System.Collections.Generic.List[string]
$markdown.Add("# $Title") | Out-Null
$markdown.Add("") | Out-Null
$markdown.Add("- Status: $status") | Out-Null
$markdown.Add("- Score: $score") | Out-Null
$markdown.Add("- Fixture count: $($FixtureRows.Count)") | Out-Null
$markdown.Add("- Fixtures ready: $fixturesReady") | Out-Null
$markdown.Add("- Scenarios pass: $scenariosPass") | Out-Null
if (-not [string]::IsNullOrWhiteSpace($ExtraDetail)) {
$markdown.Add("- Detail: $ExtraDetail") | Out-Null
}
$markdown.Add("") | Out-Null
$markdown.Add("| Scenario | Status |") | Out-Null
$markdown.Add("|---|---|") | Out-Null
foreach ($scenarioId in $ScenarioIds) {
$scenario = Get-Scenario -Rows $ScenarioRows -ScenarioId $scenarioId
$scenarioStatus = if ($scenario) { $scenario.status } else { "MISSING" }
$markdown.Add("| $scenarioId | $scenarioStatus |") | Out-Null
}
$markdown | Set-Content -Encoding UTF8 -Path $mdPath
return [pscustomobject][ordered]@{
domain = $Domain
status = $status
score = $score
fixture_count = $FixtureRows.Count
scenarios = ($ScenarioIds -join ";")
report_json = $jsonPath
report_md = $mdPath
}
}
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 ($gitStatus) {
Add-Check -Key "working_tree" -Label "working tree" -Status "warn" -Required $false -Detail "Working tree has uncommitted changes; quality 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 ($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 (Test-Path -LiteralPath $FixtureRoot -PathType Container) {
Add-Check -Key "fixture_root" -Label "quality fixture root" -Status "pass" -Detail "Fixture root exists" -Meta @{ root = $FixtureRoot }
} else {
Add-Check -Key "fixture_root" -Label "quality fixture root" -Status "fail" -Detail "Fixture root missing" -Meta @{ root = $FixtureRoot }
}
$requiredFixtureFiles = @(
"quality-metric-dictionary.csv",
"golden-review-samples.csv",
"golden-kb-question-set.csv",
"golden-contract-generation-set.csv",
"prompt-version-register.csv",
"ai-output-governance.md",
"data-retention-policy.md"
)
foreach ($fixtureFile in $requiredFixtureFiles) {
$fixturePath = Join-Path $FixtureRoot $fixtureFile
if (Test-Path -LiteralPath $fixturePath -PathType Leaf) {
Add-Check -Key ("fixture_" + ($fixtureFile -replace '[^a-zA-Z0-9]', '_')) -Label "fixture $fixtureFile" -Status "pass" -Detail "Fixture exists" -Meta @{ path = $fixturePath }
} else {
Add-Check -Key ("fixture_" + ($fixtureFile -replace '[^a-zA-Z0-9]', '_')) -Label "fixture $fixtureFile" -Status "fail" -Detail "Fixture missing" -Meta @{ path = $fixturePath }
}
}
$metrics = @(Import-Csv -LiteralPath (Join-Path $FixtureRoot "quality-metric-dictionary.csv"))
$reviewFixtures = @(Import-Csv -LiteralPath (Join-Path $FixtureRoot "golden-review-samples.csv"))
$kbFixtures = @(Import-Csv -LiteralPath (Join-Path $FixtureRoot "golden-kb-question-set.csv"))
$contractFixtures = @(Import-Csv -LiteralPath (Join-Path $FixtureRoot "golden-contract-generation-set.csv"))
$promptRegister = @(Import-Csv -LiteralPath (Join-Path $FixtureRoot "prompt-version-register.csv"))
if ($metrics.Count -ge 10) {
Add-Check -Key "metric_dictionary" -Label "quality metric dictionary" -Status "pass" -Detail "Metric count=$($metrics.Count)"
} else {
Add-Check -Key "metric_dictionary" -Label "quality metric dictionary" -Status "fail" -Detail "Metric count is too low: $($metrics.Count)"
}
$latestPhase8 = Get-LatestSummaryArtifact -Root $Phase8ArtifactRoot -SummaryFile "phase8-pilot-closed-loop-summary.json"
$phase8Summary = $null
$scenarioRows = @()
$feedbackRows = @()
$phase8AgeHours = $null
if ($latestPhase8) {
$phase8SummaryPath = Join-Path $latestPhase8.FullName "phase8-pilot-closed-loop-summary.json"
$phase8Summary = Read-JsonFile -Path $phase8SummaryPath
$phase8AgeHours = [math]::Round(((Get-Date) - $latestPhase8.LastWriteTime).TotalHours, 2)
if ($phase8Summary -and ($phase8Summary.status -eq "PASS" -or $phase8Summary.status -eq "PASS_WITH_WEBHOOK_BLOCKED") -and [int]$phase8Summary.totals.failed_required -eq 0) {
Add-Check -Key "phase8_input" -Label "phase8 closed-loop input" -Status "pass" -Detail "Phase8 status=$($phase8Summary.status), age=${phase8AgeHours}h" -Meta @{ artifact_dir = $latestPhase8.FullName; summary = $phase8SummaryPath }
} elseif ($phase8Summary) {
Add-Check -Key "phase8_input" -Label "phase8 closed-loop input" -Status "fail" -Detail "Phase8 status=$($phase8Summary.status), failed_required=$($phase8Summary.totals.failed_required)" -Meta @{ artifact_dir = $latestPhase8.FullName; summary = $phase8SummaryPath }
} else {
Add-Check -Key "phase8_input" -Label "phase8 closed-loop input" -Status "fail" -Detail "Could not parse phase8 summary" -Meta @{ artifact_dir = $latestPhase8.FullName }
}
if ($phase8AgeHours -le $MaxPhase8AgeHours) {
Add-Check -Key "phase8_freshness" -Label "phase8 evidence freshness" -Status "pass" -Detail "Phase8 artifact age=${phase8AgeHours}h"
} else {
Add-Check -Key "phase8_freshness" -Label "phase8 evidence freshness" -Status "warn" -Required $false -Detail "Phase8 artifact age ${phase8AgeHours}h exceeds $MaxPhase8AgeHours"
}
$scenarioPath = Join-Path $latestPhase8.FullName "pilot-scenario-execution-record.csv"
$feedbackPath = Join-Path $latestPhase8.FullName "pilot-feedback-register.csv"
if (Test-Path -LiteralPath $scenarioPath -PathType Leaf) {
$scenarioRows = @(Import-Csv -LiteralPath $scenarioPath)
}
if (Test-Path -LiteralPath $feedbackPath -PathType Leaf) {
$feedbackRows = @(Import-Csv -LiteralPath $feedbackPath)
}
} else {
Add-Check -Key "phase8_input" -Label "phase8 closed-loop input" -Status "fail" -Detail "No phase8 closed-loop artifact found"
}
if ($scenarioRows.Count -gt 0 -and @($scenarioRows | Where-Object { $_.status -eq "PASS" }).Count -eq $scenarioRows.Count) {
Add-Check -Key "phase8_scenarios" -Label "phase8 scenarios" -Status "pass" -Detail "Phase8 scenarios passed=$($scenarioRows.Count)/$($scenarioRows.Count)"
} else {
Add-Check -Key "phase8_scenarios" -Label "phase8 scenarios" -Status "fail" -Detail "Phase8 scenario rows missing or failed"
}
$openP0P1 = @($feedbackRows | Where-Object { ($_.severity -eq "P0" -or $_.severity -eq "P1") -and ($_.status -ne "VERIFIED" -and $_.status -ne "WONT_FIX") })
if ($openP0P1.Count -eq 0) {
Add-Check -Key "open_p0_p1_feedback" -Label "open P0/P1 feedback" -Status "pass" -Detail "Open P0/P1 feedback=0"
} else {
Add-Check -Key "open_p0_p1_feedback" -Label "open P0/P1 feedback" -Status "fail" -Detail "Open P0/P1 feedback=$($openP0P1.Count)"
}
$scorecard = New-Object System.Collections.Generic.List[object]
$scorecard.Add((Write-DomainReport -Domain "review" -Title "Phase 9 Review Quality Evaluation" -FixtureRows $reviewFixtures -ScenarioIds @("PILOT-REVIEW-001", "PILOT-REVIEW-002", "PILOT-CHAT-001", "PILOT-NEG-002") -ScenarioRows $scenarioRows -RequiredColumns @("sample_id", "expected_risk_types", "expected_evidence", "expected_actionable_suggestion", "scenario_ids") -OutputPrefix "review-quality-eval" -ExtraDetail "Review quality uses phase8 review, review detail, chat context, and failure explanation scenarios.")) | Out-Null
$scorecard.Add((Write-DomainReport -Domain "kb" -Title "Phase 9 Knowledge Base Quality Evaluation" -FixtureRows $kbFixtures -ScenarioIds @("PILOT-KB-001", "PILOT-KB-002", "PILOT-NEG-002") -ScenarioRows $scenarioRows -RequiredColumns @("question_id", "expected_source_policy", "expected_answer_behavior", "scenario_ids") -OutputPrefix "kb-quality-eval" -ExtraDetail "KB quality uses indexed document, source policy, and refusal behavior fixtures.")) | Out-Null
$scorecard.Add((Write-DomainReport -Domain "contract_generation" -Title "Phase 9 Contract Generation Quality Evaluation" -FixtureRows $contractFixtures -ScenarioIds @("PILOT-GENERATE-001", "PILOT-NEG-002") -ScenarioRows $scenarioRows -RequiredColumns @("case_id", "required_variables", "expected_preview", "expected_download", "expected_file_asset", "scenario_ids") -OutputPrefix "contract-generation-quality-eval" -ExtraDetail "Contract generation quality uses template variable, preview, download, and file asset expectations.")) | Out-Null
foreach ($domain in $scorecard) {
if ($domain.status -eq "PASS") {
Add-Check -Key ("domain_" + $domain.domain) -Label ($domain.domain + " quality") -Status "pass" -Detail "Score=$($domain.score)" -Meta @{ report = $domain.report_md }
} else {
Add-Check -Key ("domain_" + $domain.domain) -Label ($domain.domain + " quality") -Status "fail" -Detail "Score=$($domain.score)" -Meta @{ report = $domain.report_md }
}
}
$promptColumnsOk = Test-CsvColumns -Rows $promptRegister -Columns @("prompt_id", "domain", "version", "status", "expected_schema", "uncertainty_policy", "rollback_policy")
$activePromptDomains = @($promptRegister | Where-Object { $_.status -eq "active" } | Select-Object -ExpandProperty domain -Unique)
$aiGovernanceText = Get-Content -LiteralPath (Join-Path $FixtureRoot "ai-output-governance.md") -Raw -Encoding UTF8
$aiGovernanceOk = $promptColumnsOk -and ($activePromptDomains -contains "review") -and ($activePromptDomains -contains "kb") -and ($activePromptDomains -contains "contract_generation") -and $aiGovernanceText.Contains("refuse") -and $aiGovernanceText.Contains("schema")
$aiGovernanceJson = Join-Path $artifactDir "ai-output-governance-summary.json"
$aiGovernanceMd = Join-Path $artifactDir "ai-output-governance-summary.md"
$aiGovernanceSummary = [ordered]@{
status = if ($aiGovernanceOk) { "PASS" } else { "FAIL" }
prompt_count = $promptRegister.Count
active_domains = $activePromptDomains
prompt_columns_ok = $promptColumnsOk
governance_rules_present = $aiGovernanceText.Contains("Required Rules")
}
Save-Json -Path $aiGovernanceJson -Value $aiGovernanceSummary
@(
"# Phase 9 AI Output Governance Summary",
"",
"- Status: $($aiGovernanceSummary.status)",
"- Prompt count: $($promptRegister.Count)",
"- Active domains: $($activePromptDomains -join ', ')",
"- Prompt columns OK: $promptColumnsOk"
) | Set-Content -Encoding UTF8 -Path $aiGovernanceMd
if ($aiGovernanceOk) {
Add-Check -Key "ai_output_governance" -Label "AI output governance" -Status "pass" -Detail "Prompt register and governance policy are complete" -Meta @{ report = $aiGovernanceMd }
} else {
Add-Check -Key "ai_output_governance" -Label "AI output governance" -Status "fail" -Detail "AI output governance is incomplete" -Meta @{ report = $aiGovernanceMd }
}
$retentionText = Get-Content -LiteralPath (Join-Path $FixtureRoot "data-retention-policy.md") -Raw -Encoding UTF8
$secret = [string]::Concat([char]54, [char]53, [char]52, [char]51, [char]50, [char]49)
$qualityFiles = Get-ChildItem -LiteralPath $FixtureRoot -File
$secretHits = @()
foreach ($file in $qualityFiles) {
$content = Get-Content -LiteralPath $file.FullName -Raw -Encoding UTF8
if ($content.Contains($secret)) {
$secretHits += $file.FullName
}
}
$phase8SampleInventory = Join-Path $repoRoot "project-docs\phase8-pilot-templates\pilot-sample-inventory.csv"
$sampleRows = if (Test-Path -LiteralPath $phase8SampleInventory -PathType Leaf) { @(Import-Csv -LiteralPath $phase8SampleInventory) } else { @() }
$sampleColumnsOk = Test-CsvColumns -Rows $sampleRows -Columns @("sample_id", "sensitivity_level", "allowed_environment", "desensitized")
$highProductionSamples = @($sampleRows | Where-Object { $_.sensitivity_level -eq "high" -and $_.allowed_environment -eq "production" })
$dataGovernanceOk = $retentionText.Contains("Data Classes") -and $sampleColumnsOk -and $highProductionSamples.Count -eq 0 -and $secretHits.Count -eq 0
$dataGovernanceJson = Join-Path $artifactDir "data-governance-summary.json"
$dataGovernanceMd = Join-Path $artifactDir "data-governance-summary.md"
$dataGovernanceSummary = [ordered]@{
status = if ($dataGovernanceOk) { "PASS" } else { "FAIL" }
sample_count = $sampleRows.Count
sample_columns_ok = $sampleColumnsOk
high_production_samples = $highProductionSamples.Count
secret_hits = $secretHits
retention_policy_present = $retentionText.Contains("Data Classes")
}
Save-Json -Path $dataGovernanceJson -Value $dataGovernanceSummary
@(
"# Phase 9 Data Governance Summary",
"",
"- Status: $($dataGovernanceSummary.status)",
"- Sample count: $($sampleRows.Count)",
"- Sample columns OK: $sampleColumnsOk",
"- High sensitivity production samples: $($highProductionSamples.Count)",
"- Secret hits: $($secretHits.Count)"
) | Set-Content -Encoding UTF8 -Path $dataGovernanceMd
if ($dataGovernanceOk) {
Add-Check -Key "data_governance" -Label "data governance" -Status "pass" -Detail "Retention, privacy, and sample governance passed" -Meta @{ report = $dataGovernanceMd }
} else {
Add-Check -Key "data_governance" -Label "data governance" -Status "fail" -Detail "Data governance check failed" -Meta @{ report = $dataGovernanceMd }
}
$scorecard | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $scorecardCsv
$defects = New-Object System.Collections.Generic.List[object]
foreach ($feedback in $feedbackRows) {
if ($feedback.status -eq "DEFERRED") {
$defects.Add([pscustomobject][ordered]@{
defect_id = "QDEF-" + $feedback.feedback_id
source_feedback_id = $feedback.feedback_id
severity = $feedback.severity
status = "DEFERRED"
domain = if ($feedback.scenario_id -like "*WEBHOOK*") { "ops_external" } elseif ($feedback.scenario_id -like "*ROLLBACK*") { "ops_evidence" } else { "quality" }
owner = $feedback.owner
evidence_path = $feedback.evidence_path
notes = $feedback.notes
}) | Out-Null
}
}
if ($defects.Count -eq 0) {
$defects.Add([pscustomobject][ordered]@{
defect_id = "QDEF-P9-000"
source_feedback_id = ""
severity = "P3"
status = "VERIFIED"
domain = "quality"
owner = "phase9-auto"
evidence_path = $artifactDir
notes = "No deferred quality defects"
}) | Out-Null
}
$defects | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $defectCsv
$failedRequired = @($checks | Where-Object { $_.required -and $_.status -eq "fail" })
$warningsList = @($checks | Where-Object { $_.status -eq "warn" })
$blockedList = @()
if ($phase8Summary -and $phase8Summary.status -eq "PASS_WITH_WEBHOOK_BLOCKED") {
$blockedList += [pscustomobject]@{ key = "external_webhook"; detail = "Inherited BLOCKED_EXTERNAL_WEBHOOK from phase8" }
}
$overallStatus = if ($failedRequired.Count -gt 0) {
"FAIL"
} elseif ($blockedList.Count -gt 0) {
"PASS_WITH_WEBHOOK_BLOCKED"
} elseif ($warningsList.Count -gt 0) {
"WARN"
} else {
"PASS"
}
$alertSeverity = if ($failedRequired.Count -gt 0) { "critical" } elseif ($blockedList.Count -gt 0) { "blocked" } elseif ($warningsList.Count -gt 0) { "warning" } else { "ok" }
$summary = [ordered]@{
name = "phase9-business-quality-gate"
status = $overallStatus
alert_severity = $alertSeverity
generated_at = (Get-Date).ToUniversalTime().ToString("o")
artifact_dir = $artifactDir
branch = $branch
commit = $commit
remote = $remoteUrl
phase8 = [ordered]@{
artifact_dir = if ($latestPhase8) { $latestPhase8.FullName } else { $null }
status = if ($phase8Summary) { $phase8Summary.status } else { $null }
age_hours = $phase8AgeHours
scenarios_total = $scenarioRows.Count
feedback_total = $feedbackRows.Count
open_p0_p1 = $openP0P1.Count
}
scorecard = $scorecard
checks = $checks
artifacts = [ordered]@{
scorecard_csv = $scorecardCsv
defect_register = $defectCsv
ai_output_governance_summary = $aiGovernanceMd
data_governance_summary = $dataGovernanceMd
}
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 9 Business Quality 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("- Phase8 input: $($summary.phase8.artifact_dir)") | Out-Null
$markdown.Add("") | Out-Null
$markdown.Add("## Scorecard") | Out-Null
$markdown.Add("") | Out-Null
$markdown.Add("| Domain | Status | Score | Fixture Count |") | Out-Null
$markdown.Add("|---|---|---:|---:|") | Out-Null
foreach ($domain in $scorecard) {
$markdown.Add("| $($domain.domain) | $($domain.status) | $($domain.score) | $($domain.fixture_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('- `business-quality-gate-summary.json`') | Out-Null
$markdown.Add('- `business-quality-gate-summary.md`') | Out-Null
$markdown.Add('- `phase9-quality-scorecard.csv`') | Out-Null
$markdown.Add('- `phase9-quality-defect-register.csv`') | Out-Null
$markdown.Add('- `review-quality-eval.*`') | Out-Null
$markdown.Add('- `kb-quality-eval.*`') | Out-Null
$markdown.Add('- `contract-generation-quality-eval.*`') | Out-Null
$markdown.Add('- `ai-output-governance-summary.*`') | Out-Null
$markdown.Add('- `data-governance-summary.*`') | Out-Null
$markdown | Set-Content -Encoding UTF8 -Path $summaryMd
Write-Host "PHASE9_BUSINESS_QUALITY_GATE_$overallStatus $artifactDir"
if ($overallStatus -eq "FAIL" -or ($FailOnWarn -and $overallStatus -eq "WARN") -or ($FailOnExternalBlocked -and $overallStatus -eq "PASS_WITH_WEBHOOK_BLOCKED")) {
throw "Phase 9 business quality gate $overallStatus. See $summaryMd"
}