151 lines
5.7 KiB
PowerShell
151 lines
5.7 KiB
PowerShell
<#
|
||
.SYNOPSIS
|
||
Verify that a code-review report carries a line-coverage gate (fail-closed).
|
||
|
||
.DESCRIPTION
|
||
Enforces the "three-piece suite" guarantee defined in project-review/SKILL.md
|
||
Step 8.7: whole-project / feature review reports MUST contain a line coverage
|
||
(xingfugai) field in their coverage section, and its value MUST be >= the
|
||
configured target (default 95%). A missing field means the review never ran
|
||
coverage_tool(gate="both+line") (or forgot the three-piece data) and the
|
||
report would otherwise look green with no line coverage.
|
||
|
||
Scope handling: only whole-project and feature reviews are gated; other
|
||
scopes (e.g. change-level) are skipped.
|
||
|
||
Exit codes: 0 = line coverage present and >= target (or scope not gated);
|
||
1 = blocking violation (missing line coverage or below target).
|
||
|
||
.PARAMETER Repo
|
||
Repository root. Defaults to the current directory.
|
||
|
||
.PARAMETER Report
|
||
Optional relative path (under docs/reviews/) to a specific report file.
|
||
Defaults to the newest *.md report in docs/reviews/.
|
||
|
||
.PARAMETER Target
|
||
Minimum line-coverage percentage. Default 95.
|
||
|
||
.EXAMPLE
|
||
powershell -File verify-line-coverage.ps1 -Repo D:\AuraSpace
|
||
powershell -File verify-line-coverage.ps1 -Repo D:\AuraSpace -Report full-project-review-2026-08-17-101500.md
|
||
#>
|
||
param(
|
||
[string]$Repo = "",
|
||
[string]$Report = "",
|
||
[double]$Target = 95.0
|
||
)
|
||
|
||
$ErrorActionPreference = "Stop"
|
||
|
||
function Write-Step([string]$msg) { Write-Host " $msg" -ForegroundColor DarkGray }
|
||
function Write-Ok([string]$msg) { Write-Host " [OK] $msg" -ForegroundColor Green }
|
||
function Write-Bad([string]$msg) { Write-Host " [!!] $msg" -ForegroundColor Red }
|
||
function Write-Warn([string]$msg) { Write-Host " [!] $msg" -ForegroundColor Yellow }
|
||
|
||
# Resolve repo root ---------------------------------------------------------
|
||
if ([string]::IsNullOrWhiteSpace($Repo)) { $Repo = (Get-Location).Path }
|
||
$root = (Resolve-Path -LiteralPath $Repo).Path
|
||
$reviewsDir = Join-Path $root "docs\reviews"
|
||
|
||
Write-Host "verify-line-coverage: repo = $root (target $Target%)"
|
||
Write-Host ""
|
||
|
||
# Locate the report --------------------------------------------------------
|
||
$reportPath = ""
|
||
if (-not [string]::IsNullOrWhiteSpace($Report)) {
|
||
$reportPath = Join-Path $reviewsDir $Report
|
||
if (-not (Test-Path -LiteralPath $reportPath)) {
|
||
Write-Bad "report not found: $reportPath"
|
||
exit 1
|
||
}
|
||
} else {
|
||
if (-not (Test-Path -LiteralPath $reviewsDir)) {
|
||
Write-Warn "docs/reviews/ does not exist - no report to verify"
|
||
Write-Host "RESULT: PASS (no report)" -ForegroundColor Green
|
||
exit 0
|
||
}
|
||
$reportPath = Get-ChildItem -LiteralPath $reviewsDir -File |
|
||
Where-Object { $_.Extension -eq ".md" } |
|
||
Sort-Object LastWriteTime -Descending |
|
||
Select-Object -First 1 -ExpandProperty FullName
|
||
if (-not $reportPath) {
|
||
Write-Warn "no .md report in docs/reviews/ - nothing to verify"
|
||
Write-Host "RESULT: PASS (no report)" -ForegroundColor Green
|
||
exit 0
|
||
}
|
||
}
|
||
|
||
Write-Step "checking report: $reportPath"
|
||
|
||
# Parse scope from header (line like: 档位:standard · 范围:whole-project · 基线:...)
|
||
$content = [System.IO.File]::ReadAllText($reportPath, [System.Text.Encoding]::UTF8)
|
||
$scope = ""
|
||
$scopeLine = ($content -split "`n" | Where-Object { $_ -match "范围" } | Select-Object -First 1)
|
||
if ($scopeLine -and $scopeLine -match "范围\**[::]\s*([^\s·*]+)") {
|
||
$scope = $Matches[1].Trim()
|
||
}
|
||
|
||
# Scope gate: only change-level reviews are skipped. whole-project / feature
|
||
# AND any custom scope value (e.g. scope="evm") that still produced a coverage
|
||
# section must be checked for line coverage.
|
||
if ($scope -eq "change-level") {
|
||
Write-Warn "scope = '$scope' is not gated on line coverage (change-level only)"
|
||
Write-Host "RESULT: PASS (scope not gated)" -ForegroundColor Green
|
||
exit 0
|
||
}
|
||
Write-Step "scope = '$scope' (gated)"
|
||
|
||
# Parse ## 覆盖度 section
|
||
$covSection = ""
|
||
$lines = $content -split "`r?`n"
|
||
for ($i = 0; $i -lt $lines.Count; $i++) {
|
||
if ($lines[$i] -match "^##\s*\**覆盖度\**") {
|
||
$j = $i + 1
|
||
$buf = @()
|
||
while ($j -lt $lines.Count -and -not ($lines[$j] -match "^##\s")) {
|
||
$buf += $lines[$j]; $j++
|
||
}
|
||
$covSection = $buf -join "`n"
|
||
break
|
||
}
|
||
}
|
||
if (-not $covSection) {
|
||
Write-Bad "no coverage section found - line coverage cannot be verified"
|
||
exit 1
|
||
}
|
||
|
||
# Line-coverage field present?
|
||
$lineLine = ($covSection -split "`n" | Where-Object { $_ -match "行覆盖" } | Select-Object -First 1)
|
||
if (-not $lineLine) {
|
||
Write-Bad "coverage section has NO line-coverage field - gate='both+line' was not run (or three-piece data missing)"
|
||
Write-Step "coverage section:"
|
||
foreach ($l in ($covSection -split "`n" | Where-Object { $_.Trim() })) { Write-Step " $($l.Trim())" }
|
||
exit 1
|
||
}
|
||
|
||
# "未执行" marker means gate never ran
|
||
if ($lineLine -match "未执行") {
|
||
Write-Bad "line coverage = NOT RUN (gate='both+line' not run or three-piece data missing)"
|
||
Write-Step $lineLine.Trim()
|
||
exit 1
|
||
}
|
||
|
||
# Extract percentage (行覆盖:99.8% — 目标 95.0%(缺口 0 文件)✅)
|
||
if ($lineLine -match "行覆盖\**[::]\s*([0-9.]+)\s*%") {
|
||
$pct = [double]$Matches[1]
|
||
if ($pct -lt $Target) {
|
||
Write-Bad "line coverage $pct% < target $Target%"
|
||
Write-Step $lineLine.Trim()
|
||
exit 1
|
||
}
|
||
Write-Ok "line coverage = $pct% (>= $Target%)"
|
||
if ($lineLine -match "缺口\s*([0-9]+)\s*文件" -and [int]$Matches[1] -gt 0) {
|
||
Write-Warn "line-coverage gap files present: $($Matches[1])"
|
||
}
|
||
Write-Host "RESULT: PASS - line coverage verified" -ForegroundColor Green
|
||
exit 0
|
||
}
|
||
|
||
Write-Bad "unable to parse line-coverage percentage from: $($lineLine.Trim())"
|
||
exit 1 |