<# .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