extensions/specrew-speckit/scripts/scaffold-feature-closeout-dashboard.ps1

[CmdletBinding()]
param(
    [string]$ProjectPath = '.',
    [string]$FeatureId,
    [switch]$DryRun,
    [switch]$PassThru
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

$sharedGovernancePath = Join-Path $PSScriptRoot 'shared-governance.ps1'
if (-not (Test-Path -LiteralPath $sharedGovernancePath -PathType Leaf)) {
    throw "Missing shared governance helper '$sharedGovernancePath'."
}
. $sharedGovernancePath

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 Get-FeatureCloseoutIdentityPath {
    param(
        [Parameter(Mandatory = $true)][string]$ResolvedProjectPath
    )

    return Join-Path $ResolvedProjectPath '.squad\identity\now.md'
}

function Get-FeatureCloseoutNumberLabel {
    param(
        [Parameter(Mandatory = $true)][string]$FeatureRef
    )

    if ($FeatureRef -match '^(?<number>\d{3})') {
        return "Feature $($Matches['number'])"
    }

    return $FeatureRef
}

function Get-NextRoadmapItemLabel {
    param(
        [Parameter(Mandatory = $true)][string]$ResolvedProjectPath
    )

    if (-not (Get-Command Read-SpecrewRoadmapDefinition -ErrorAction SilentlyContinue)) {
        return 'Roadmap update pending'
    }

    $roadmapDefinition = Read-SpecrewRoadmapDefinition -ProjectRoot $ResolvedProjectPath
    $nextPhase = @($roadmapDefinition.phases | Where-Object { $_.status -eq 'queued' } | Select-Object -First 1)
    if ($nextPhase.Count -eq 0) {
        $nextPhase = @($roadmapDefinition.phases | Where-Object { $_.status -ne 'shipped' } | Select-Object -First 1)
    }

    if ($nextPhase.Count -gt 0 -and -not [string]::IsNullOrWhiteSpace([string]$nextPhase[0].name)) {
        return [string]$nextPhase[0].name
    }

    return 'Roadmap update pending'
}

function Get-FeatureCloseoutIdentityBody {
    param(
        [Parameter(Mandatory = $true)][string]$ResolvedProjectPath,
        [Parameter(Mandatory = $true)][string]$FeatureRef
    )

    $timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')
    $featureLabel = Get-FeatureCloseoutNumberLabel -FeatureRef $FeatureRef
    $nextRoadmapItem = Get-NextRoadmapItemLabel -ResolvedProjectPath $ResolvedProjectPath
    return @(
        '# What We''re Focused On'
        ''
        ('No active feature. Last completed: {0} at {1}. Next roadmap item: {2} (not yet authorized).' -f $featureLabel, $timestamp, $nextRoadmapItem)
        ''
    ) -join [Environment]::NewLine
}

$syncBoundaryStateScript = Join-Path $PSScriptRoot 'sync-boundary-state.ps1'
if (-not (Test-Path -LiteralPath $syncBoundaryStateScript -PathType Leaf)) {
    throw "Missing boundary-state sync helper '$syncBoundaryStateScript'."
}

function Get-ResolvedFeatureDirectory {
    param(
        [Parameter(Mandatory = $true)][string]$ResolvedProjectPath,
        [AllowNull()][string]$FeatureId
    )

    if (-not [string]::IsNullOrWhiteSpace($FeatureId)) {
        return Join-Path $ResolvedProjectPath ('specs\' + $FeatureId)
    }

    $featureJsonPath = Join-Path $ResolvedProjectPath '.specify\feature.json'
    if (-not (Test-Path -LiteralPath $featureJsonPath -PathType Leaf)) {
        throw "Cannot resolve the feature-closeout dashboard target because '.specify\feature.json' is missing."
    }

    $featureJson = Get-Content -LiteralPath $featureJsonPath -Raw -Encoding UTF8 | ConvertFrom-Json -AsHashtable -Depth 12

    # F-023: Legacy schema handling - missing 'schema' field implies v0
    $schema = Get-SpecrewStateSchemaVersion -State $featureJson -Path $featureJsonPath
    # v0 behavior: feature_directory field is required (old closeout scripts expect it)
    # v1+ behavior: same as v0 for this field (no behavioral divergence yet)

    if ([string]::IsNullOrWhiteSpace([string]$featureJson['feature_directory'])) {
        throw "Cannot resolve the feature-closeout dashboard target because '.specify\feature.json' does not contain feature_directory."
    }

    $candidate = [string]$featureJson['feature_directory']
    if (-not [System.IO.Path]::IsPathRooted($candidate)) {
        $candidate = Join-Path $ResolvedProjectPath $candidate
    }

    return [System.IO.Path]::GetFullPath($candidate)
}

$resolvedProjectPath = (Resolve-Path -Path (Resolve-ProjectPath -Path $ProjectPath)).Path
$featureDirectory = Get-ResolvedFeatureDirectory -ResolvedProjectPath $resolvedProjectPath -FeatureId $FeatureId
if (-not (Test-Path -LiteralPath $featureDirectory -PathType Container)) {
    throw "Feature directory '$featureDirectory' does not exist."
}

$featureRef = Split-Path -Leaf $featureDirectory
$targetPath = Join-Path $featureDirectory 'closeout-dashboard.md'
$rendererPath = Join-Path $resolvedProjectPath 'scripts\internal\dashboard-renderer.ps1'
$rendererAvailable = Test-Path -LiteralPath $rendererPath -PathType Leaf
if ($rendererAvailable) {
    . $rendererPath
}

$actions = [System.Collections.ArrayList]::new()
if (Test-Path -LiteralPath $targetPath -PathType Leaf) {
    Add-ScaffoldAction -Actions $actions -Action 'preserved' -Path $targetPath
}
else {
    if ($DryRun) {
        Add-ScaffoldAction -Actions $actions -Action 'would-create' -Path $targetPath
    }
    elseif (-not $rendererAvailable) {
        Write-Output ("WARN [dashboard] Velocity dashboard renderer '{0}' is missing; feature closeout snapshot not generated." -f $rendererPath)
        Add-ScaffoldAction -Actions $actions -Action 'warning' -Path $targetPath
    }
    else {
        try {
            $snapshotParameters = @{
                ProjectRoot = $resolvedProjectPath
                FeatureId   = $featureRef
            }
            if ((Get-Command Get-SpecrewDashboardSnapshot -ErrorAction SilentlyContinue).Parameters.ContainsKey('CaptureKind')) {
                $snapshotParameters.CaptureKind = 'feature-closeout'
            }
            $snapshot = Get-SpecrewDashboardSnapshot @snapshotParameters
            $lines = ConvertTo-SpecrewDashboardLines -Snapshot $snapshot
            $content = ConvertTo-SpecrewDashboardArtifactContent -Snapshot $snapshot -Lines $lines -CaptureKind 'feature-closeout' -HistoricalNotice $null
            Write-Utf8FileAtomic -Path $targetPath -Content $content
            Add-ScaffoldAction -Actions $actions -Action 'created' -Path $targetPath
        }
        catch {
            Write-Output ("WARN [dashboard] Unable to generate feature closeout snapshot: {0}" -f $_.Exception.Message)
            Add-ScaffoldAction -Actions $actions -Action 'warning' -Path $targetPath
        }
    }
}

if (-not $DryRun) {
    $identityPath = Get-FeatureCloseoutIdentityPath -ResolvedProjectPath $resolvedProjectPath
    $identityBody = Get-FeatureCloseoutIdentityBody -ResolvedProjectPath $resolvedProjectPath -FeatureRef $featureRef
    Add-ScaffoldAction -Actions $actions -Action 'updated' -Path $identityPath
    & $syncBoundaryStateScript -ProjectPath $resolvedProjectPath -BoundaryType 'feature-closeout' -FeatureRef $featureRef -IdentityFocusArea 'No active feature' -IdentityActiveIssues '[]' -IdentityBody $identityBody -PassThru | Out-Null
}

if ($PassThru) {
    $actions
    return
}

$actions | Select-Object Action, Path | Format-Table -AutoSize
Write-Output ("Feature closeout dashboard scaffold {0} for {1}" -f ($(if ($DryRun) { 'previewed' } else { 'completed' }), $targetPath))
exit 0