extensions/specrew-speckit/scripts/scaffold-review-artifact.ps1
|
[CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$IterationDirectory, [ValidateSet('accepted', 'needs-rework', 'blocked')] [string]$OverallVerdict = 'needs-rework', [ValidateSet('pass', 'needs-work', 'blocked')] [string]$DefaultTaskVerdict = 'needs-work', [string]$ReviewedDate = (Get-Date -Format 'yyyy-MM-dd'), [switch]$DryRun, [switch]$PassThru ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' function Add-ScaffoldAction { param( [AllowEmptyCollection()] [Parameter(Mandatory = $true)] [System.Collections.ArrayList]$Actions, [Parameter(Mandatory = $true)] [string]$Action, [Parameter(Mandatory = $true)] [string]$Path ) $null = $Actions.Add([pscustomobject]@{ Action = $Action Path = $Path }) } function Write-MissingFile { param( [Parameter(Mandatory = $true)] [string]$TargetPath, [Parameter(Mandatory = $true)] [string]$Content, [AllowEmptyCollection()] [Parameter(Mandatory = $true)] [System.Collections.ArrayList]$Actions ) if (Test-Path -LiteralPath $TargetPath) { Add-ScaffoldAction -Actions $Actions -Action 'preserved' -Path $TargetPath return } Add-ScaffoldAction -Actions $Actions -Action $(if ($DryRun) { 'would-create' } else { 'created' }) -Path $TargetPath if (-not $DryRun) { $parent = Split-Path -Parent $TargetPath if (-not (Test-Path -LiteralPath $parent)) { New-Item -ItemType Directory -Path $parent -Force | Out-Null } [System.IO.File]::WriteAllText($TargetPath, $Content, [System.Text.UTF8Encoding]::new($false)) } } function Get-MarkdownContent { param([string]$Path) return @(Get-Content -LiteralPath $Path -Encoding UTF8) } function Get-MarkdownSectionTable { param( [AllowEmptyString()] [string[]]$Lines, [string]$Heading ) $headingPattern = '^##\s+' + [regex]::Escape($Heading) + '\b' $startIndex = -1 for ($index = 0; $index -lt $Lines.Count; $index++) { if ($Lines[$index] -match $headingPattern) { $startIndex = $index break } } if ($startIndex -lt 0) { return @() } $tableLines = New-Object System.Collections.Generic.List[string] for ($index = $startIndex + 1; $index -lt $Lines.Count; $index++) { $currentLine = $Lines[$index] if ($currentLine -match '^##\s+') { break } if ($currentLine.Trim().StartsWith('|')) { $null = $tableLines.Add($currentLine) } } if ($tableLines.Count -lt 2) { return @() } $headers = ($tableLines[0].Trim('|') -split '\|') | ForEach-Object { $_.Trim() } $rows = New-Object System.Collections.Generic.List[object] for ($rowIndex = 1; $rowIndex -lt $tableLines.Count; $rowIndex++) { $cells = ($tableLines[$rowIndex].Trim('|') -split '\|') | ForEach-Object { $_.Trim() } $isSeparator = $true foreach ($cell in $cells) { if ($cell -notmatch '^:?-{3,}:?$') { $isSeparator = $false break } } if ($isSeparator) { continue } $row = [ordered]@{} for ($cellIndex = 0; $cellIndex -lt $headers.Count; $cellIndex++) { $value = if ($cellIndex -lt $cells.Count) { $cells[$cellIndex] } else { '' } $row[$headers[$cellIndex]] = $value } $rows.Add([pscustomobject]$row) } return $rows.ToArray() } function Get-TaskReviewNote { param( [string]$TaskStatus, [string]$RequirementRef ) $normalized = $TaskStatus.Trim().ToLowerInvariant() switch ($normalized) { 'done' { return "Review delivered output against $RequirementRef and adjust verdict if needed." } 'blocked' { return 'Execution reported blocked; confirm blocker status and escalation path.' } 'deferred' { return 'Deferred work; confirm the deferral is still acceptable for this iteration.' } 'needs-rework' { return 'Task already marked needs-rework during execution; confirm re-entry scope.' } default { return 'Populate verdict after reviewing the delivered evidence.' } } } function Get-IterationLabel { param( [AllowEmptyString()] [string[]]$PlanLines, [string]$Fallback ) $titleLine = @($PlanLines | Select-Object -First 1)[0] if (-not [string]::IsNullOrWhiteSpace($titleLine) -and $titleLine -match '^#\s+Iteration Plan:\s+(.+?)(?:\s+\(stub\))?\s*$') { return $Matches[1].Trim() } return $Fallback } $resolvedIterationDirectory = [System.IO.Path]::GetFullPath($IterationDirectory) $planPath = Join-Path $resolvedIterationDirectory 'plan.md' $reviewPath = Join-Path $resolvedIterationDirectory 'review.md' $actions = [System.Collections.ArrayList]::new() if (-not (Test-Path -LiteralPath $planPath)) { throw "Iteration plan '$planPath' does not exist." } $planLines = @(Get-MarkdownContent -Path $planPath) $tasks = @(Get-MarkdownSectionTable -Lines $planLines -Heading 'Tasks') if ($tasks.Count -eq 0) { throw "Plan '$planPath' does not contain a populated Tasks table." } $iterationLabel = Get-IterationLabel -PlanLines $planLines -Fallback (Split-Path -Leaf $resolvedIterationDirectory) $verdictRows = @( '| Task | Requirement | Verdict | Notes |' '| ---- | ----------- | ------- | ----- |' ) foreach ($task in $tasks) { $taskId = [string]$task.Task if ([string]::IsNullOrWhiteSpace($taskId)) { continue } $requirementRef = [string]$task.Requirement $taskStatus = [string]$task.Status $note = Get-TaskReviewNote -TaskStatus $taskStatus -RequirementRef $requirementRef $verdictRows += ('| {0} | {1} | {2} | {3} |' -f $taskId.Trim(), $requirementRef.Trim(), $DefaultTaskVerdict, ($note -replace '\|', '\|')) } $reviewContent = @" # Review: Iteration $iterationLabel **Schema**: v1 **Reviewed**: $ReviewedDate **Overall Verdict**: $OverallVerdict ## Task Verdicts $($verdictRows -join [Environment]::NewLine) ## Gap Ledger - Replace this reminder with either: (a) `No known gaps remain.` or (b) explicit gap entries covering the affected requirement/artifact, whether the gap is fixed now or deferred with approval, and any required spec/plan/tasks updates. ## Notes - This artifact was scaffolded from plan.md for the Review/Demo ceremony. - Replace default verdicts with the actual per-task review outcome before closing the review phase. - Use the no-gap policy: known gaps must be fixed now or explicitly deferred with approval and recorded evidence before closure. - If per-task drift checks did not run during execution, invoke `specrew-drift-check` in batch and update drift-log.md before accepting the iteration. "@ Write-MissingFile -TargetPath $reviewPath -Content $reviewContent -Actions $actions if ($PassThru) { $actions return } $actions | Select-Object Action, Path | Format-Table -AutoSize Write-Host ("Review artifact scaffold {0} for {1}" -f ($(if ($DryRun) { 'previewed' } else { 'completed' }), $reviewPath)) -ForegroundColor Green exit 0 |