scripts/internal/dashboard-renderer.ps1

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

$sharedGovernancePath = Join-Path (Split-Path -Parent (Split-Path -Parent $PSScriptRoot)) 'extensions\specrew-speckit\scripts\shared-governance.ps1'
if (-not (Test-Path -LiteralPath $sharedGovernancePath -PathType Leaf)) {
    throw "Missing shared governance helper '$sharedGovernancePath'."
}
. $sharedGovernancePath

function Reset-SpecrewDashboardWarnings {
    $script:SpecrewDashboardWarnings = New-Object System.Collections.Generic.List[string]
}

function Add-SpecrewDashboardWarning {
    param([AllowNull()][string]$Message)

    if ([string]::IsNullOrWhiteSpace($Message)) {
        return
    }

    if (-not (Get-Variable -Name SpecrewDashboardWarnings -Scope Script -ErrorAction SilentlyContinue)) {
        Reset-SpecrewDashboardWarnings
    }

    $script:SpecrewDashboardWarnings.Add($Message) | Out-Null
}

function Get-SpecrewDashboardWarnings {
    if (-not (Get-Variable -Name SpecrewDashboardWarnings -Scope Script -ErrorAction SilentlyContinue)) {
        return @()
    }

    return @($script:SpecrewDashboardWarnings | Select-Object -Unique)
}

function Get-SpecrewMarkdownContent {
    param([Parameter(Mandatory = $true)][string]$Path)

    return [System.IO.File]::ReadAllLines($Path, [System.Text.Encoding]::UTF8)
}

function Get-SpecrewMarkdownMetadataValue {
    param(
        [AllowNull()]
        [AllowEmptyCollection()]
        [AllowEmptyString()]
        [Parameter(Mandatory = $true)][string[]]$Lines,
        [Parameter(Mandatory = $true)][string]$Label
    )

    $pattern = '^\*\*' + [regex]::Escape($Label) + '\*\*:\s*(.+?)\s*$'
    foreach ($line in $Lines) {
        if ($line -match $pattern) {
            return $Matches[1].Trim()
        }
    }

    return $null
}

function Get-SpecrewMarkdownHeadingValue {
    param(
        [AllowNull()]
        [AllowEmptyCollection()]
        [AllowEmptyString()]
        [Parameter(Mandatory = $true)][string[]]$Lines,
        [Parameter(Mandatory = $false)][string]$Fallback = ''
    )

    foreach ($line in $Lines) {
        if ($line -match '^#\s+(.+?)\s*$') {
            return $Matches[1].Trim()
        }
    }

    return $Fallback
}

function Get-SpecrewMarkdownSectionTable {
    param(
        [AllowNull()]
        [AllowEmptyCollection()]
        [AllowEmptyString()]
        [Parameter(Mandatory = $true)][string[]]$Lines,
        [Parameter(Mandatory = $true)][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++) {
            $row[$headers[$cellIndex]] = if ($cellIndex -lt $cells.Count) { $cells[$cellIndex] } else { '' }
        }

        $rows.Add([pscustomobject]$row) | Out-Null
    }

    return $rows.ToArray()
}

function Get-SpecrewYamlScalarValue {
    param([AllowNull()][string]$Value)

    if ($null -eq $Value) {
        return $null
    }

    $text = $Value.Trim()
    if ($text.Contains('#')) {
        $text = ($text -split '\s+#', 2)[0].Trim()
    }

    if (($text.StartsWith('"') -and $text.EndsWith('"')) -or ($text.StartsWith("'") -and $text.EndsWith("'"))) {
        $text = $text.Substring(1, $text.Length - 2)
    }

    if ($text -eq 'null') {
        return $null
    }

    return $text
}

function ConvertTo-SpecrewNullableDecimal {
    param([AllowNull()][string]$Value)

    if ([string]::IsNullOrWhiteSpace($Value)) {
        return $null
    }

    $match = [regex]::Match($Value, '-?\d+(?:\.\d+)?')
    if (-not $match.Success) {
        return $null
    }

    return [decimal]::Parse($match.Value, [System.Globalization.CultureInfo]::InvariantCulture)
}

function ConvertTo-SpecrewNullableDate {
    param([AllowNull()][string]$Value)

    if ([string]::IsNullOrWhiteSpace($Value)) {
        return $null
    }

    $match = [regex]::Match($Value, '\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}:\d{2}Z)?')
    if (-not $match.Success) {
        return $null
    }

    try {
        return [datetime]::Parse($match.Value, [System.Globalization.CultureInfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::AssumeUniversal)
    }
    catch {
        return $null
    }
}

function ConvertTo-SpecrewTitleCase {
    param([AllowNull()][string]$Value)

    if ([string]::IsNullOrWhiteSpace($Value)) {
        return ''
    }

    return [System.Globalization.CultureInfo]::InvariantCulture.TextInfo.ToTitleCase($Value.ToLowerInvariant())
}

function Get-SpecrewFeatureNumber {
    param([AllowNull()][string]$FeatureRef)

    if ([string]::IsNullOrWhiteSpace($FeatureRef)) {
        return $null
    }

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

    return $FeatureRef
}

function Format-SpecrewIterationIdentifier {
    param(
        [Parameter(Mandatory = $true)][string]$FeatureRef,
        [Parameter(Mandatory = $true)][string]$IterationRef
    )

    $featureNumber = Get-SpecrewFeatureNumber -FeatureRef $FeatureRef
    if ([string]::IsNullOrWhiteSpace($featureNumber)) {
        return $IterationRef
    }

    return "feature-$featureNumber.iter-$IterationRef"
}

function Get-SpecrewFeatureShortLabel {
    param([AllowNull()][string]$FeatureRef)

    $featureNumber = Get-SpecrewFeatureNumber -FeatureRef $FeatureRef
    if ([string]::IsNullOrWhiteSpace($featureNumber)) {
        return $FeatureRef
    }

    return "feature-$featureNumber"
}

function Get-SpecrewGitBranch {
    param([Parameter(Mandatory = $true)][string]$ProjectRoot)

    $gitCommand = Get-Command -Name 'git' -ErrorAction SilentlyContinue
    if ($null -eq $gitCommand) {
        return '(git unavailable)'
    }

    $branch = @(& git -C $ProjectRoot rev-parse --abbrev-ref HEAD 2>$null)
    if ($LASTEXITCODE -ne 0 -or $branch.Count -eq 0) {
        return '(detached)'
    }

    return ([string]$branch[0]).Trim()
}

function Get-SpecrewBoundaryCatalog {
    return @(
        [pscustomobject]@{ Name = 'planning'; Pattern = '^Feature \d+.* iteration \d+ planning boundary(?:\s|$)' },
        [pscustomobject]@{ Name = 'iteration-closeout'; Pattern = '^Feature \d+.* iteration \d+ closeout boundary(?:\s|$)' },
        [pscustomobject]@{ Name = 'feature-closeout'; Pattern = '^Feature \d+.*: feature-closeout boundary(?:\s|$)' }
    )
}

function Get-SpecrewBoundaryCommitMatch {
    param([AllowNull()][string]$Subject)

    if ([string]::IsNullOrWhiteSpace($Subject)) {
        return $null
    }

    foreach ($boundary in Get-SpecrewBoundaryCatalog) {
        if ($Subject -match $boundary.Pattern) {
            $featureNumber = if ($Subject -match 'Feature\s+(?<feature>\d+)') { [int]$Matches['feature'] } else { $null }
            $iterationNumber = if ($Subject -match 'iteration\s+(?<iteration>\d+)') { [int]$Matches['iteration'] } else { $null }
            return [pscustomobject]@{
                Boundary        = $boundary.Name
                FeatureNumber   = $featureNumber
                IterationNumber = $iterationNumber
                Subject         = $Subject.Trim()
            }
        }
    }

    return $null
}

function Get-SpecrewGitBoundaryCommits {
    param([Parameter(Mandatory = $true)][string]$ProjectRoot)

    $gitCommand = Get-Command -Name 'git' -ErrorAction SilentlyContinue
    if ($null -eq $gitCommand) {
        return @()
    }

    $rawLog = @(git -C $ProjectRoot --no-pager log --grep='boundary' --regexp-ignore-case --format='%H%x09%cI%x09%s' 2>$null)
    if ($LASTEXITCODE -ne 0) {
        return @()
    }

    $records = New-Object System.Collections.Generic.List[object]
    foreach ($line in $rawLog) {
        if ([string]::IsNullOrWhiteSpace($line)) {
            continue
        }

        $parts = $line -split "`t", 3
        if ($parts.Count -lt 3) {
            continue
        }

        $boundaryMatch = Get-SpecrewBoundaryCommitMatch -Subject $parts[2]
        if ($null -eq $boundaryMatch) {
            continue
        }

        $committedAt = $null
        try {
            $committedAt = [DateTimeOffset]::Parse($parts[1]).UtcDateTime
        }
        catch {
            continue
        }

        $records.Add([pscustomobject]@{
                CommitHash      = $parts[0]
                CommittedAt     = $committedAt
                Boundary        = $boundaryMatch.Boundary
                FeatureNumber   = $boundaryMatch.FeatureNumber
                IterationNumber = $boundaryMatch.IterationNumber
                Subject         = $boundaryMatch.Subject
            }) | Out-Null
    }

    return $records.ToArray()
}

function Get-SpecrewBoundaryCommitTimestamp {
    param(
        [AllowEmptyCollection()][object[]]$BoundaryCommits,
        [Parameter(Mandatory = $true)][string]$Boundary,
        [AllowNull()][int]$FeatureNumber,
        [AllowNull()][int]$IterationNumber,
        [switch]$Latest
    )

    if ($BoundaryCommits.Count -eq 0 -or $null -eq $FeatureNumber) {
        return $null
    }

    $matches = @(
        $BoundaryCommits |
            Where-Object {
                $_.Boundary -eq $Boundary -and
                $_.FeatureNumber -eq $FeatureNumber -and
                ($null -eq $IterationNumber -or $_.IterationNumber -eq $IterationNumber)
            }
    )

    if ($matches.Count -eq 0) {
        return $null
    }

    $ordered = if ($Latest) { $matches | Sort-Object CommittedAt -Descending } else { $matches | Sort-Object CommittedAt }
    return $ordered[0].CommittedAt
}

function Get-SpecrewCalendarDayDuration {
    param(
        [Parameter(Mandatory = $true)][datetime]$Start,
        [Parameter(Mandatory = $true)][datetime]$End
    )

    $startDate = $Start.Date
    $endDate = $End.Date
    if ($endDate -lt $startDate) {
        return 1
    }

    return [math]::Max(1, [int](($endDate - $startDate).TotalDays + 1))
}

function Get-SpecrewActiveFeatureDirectory {
    param([Parameter(Mandatory = $true)][string]$ProjectRoot)

    $featureJsonPath = Join-Path $ProjectRoot '.specify\feature.json'
    if (-not (Test-Path -LiteralPath $featureJsonPath -PathType Leaf)) {
        return $null
    }

    try {
        $featureJson = Get-Content -LiteralPath $featureJsonPath -Raw -Encoding UTF8 | ConvertFrom-Json
        if ([string]::IsNullOrWhiteSpace([string]$featureJson.feature_directory)) {
            return $null
        }

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

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

function Get-SpecrewDeliveredStoryPoints {
    param(
        [Parameter(Mandatory = $true)][string]$IterationDirectory,
        [AllowEmptyCollection()][string[]]$RetroLines = @(),
        [AllowEmptyCollection()][string[]]$StateLines = @()
    )

    $retroPath = Join-Path $IterationDirectory 'retro.md'
    if (@($RetroLines).Count -eq 0 -and (Test-Path -LiteralPath $retroPath -PathType Leaf)) {
        $RetroLines = @(Get-SpecrewMarkdownContent -Path $retroPath)
    }

    if (@($RetroLines).Count -gt 0) {
        foreach ($line in @($RetroLines)) {
            if ($line -match '^\|\s*\*\*Total Effort\*\*\s*\|\s*\*\*(?<planned>[^|]+)\*\*\s*\|\s*\*\*(?<actual>[^|]+)\*\*') {
                $actual = ConvertTo-SpecrewNullableDecimal -Value $Matches['actual']
                if ($null -ne $actual) {
                    return $actual
                }
            }
            if ($line -match '^\|\s*Actual Effort\s*\|\s*(?<actual>[^|]+)\|') {
                $actual = ConvertTo-SpecrewNullableDecimal -Value $Matches['actual']
                if ($null -ne $actual) {
                    return $actual
                }
            }
        }
    }

    $statePath = Join-Path $IterationDirectory 'state.md'
    if (@($StateLines).Count -eq 0 -and (Test-Path -LiteralPath $statePath -PathType Leaf)) {
        $StateLines = @(Get-SpecrewMarkdownContent -Path $statePath)
    }

    if (@($StateLines).Count -gt 0) {
        foreach ($line in @($StateLines)) {
            if ($line -match '^\|\s*\*\*Total Story Points(?: \(Iteration \d+\))?\*\*\s*\|\s*(?<value>[^|]+)\|') {
                $total = ConvertTo-SpecrewNullableDecimal -Value $Matches['value']
                if ($null -ne $total) {
                    return $total
                }
            }
        }
    }

    return [decimal]0
}

function Get-SpecrewPlannedStoryPoints {
    param(
        [Parameter(Mandatory = $true)][string]$IterationDirectory,
        [AllowEmptyCollection()][string[]]$PlanLines = @(),
        [AllowEmptyCollection()][string[]]$StateLines = @()
    )

    $planPath = Join-Path $IterationDirectory 'plan.md'
    if (@($PlanLines).Count -eq 0 -and (Test-Path -LiteralPath $planPath -PathType Leaf)) {
        $PlanLines = @(Get-SpecrewMarkdownContent -Path $planPath)
    }

    if (@($PlanLines).Count -gt 0) {
        $capacityLine = Get-SpecrewMarkdownMetadataValue -Lines @($PlanLines) -Label 'Capacity'
        $capacity = ConvertTo-SpecrewNullableDecimal -Value $capacityLine
        if ($null -ne $capacity) {
            return $capacity
        }
    }

    $sum = [decimal]0
    if (@($PlanLines).Count -gt 0) {
        $taskRows = @(Get-SpecrewMarkdownSectionTable -Lines @($PlanLines) -Heading 'Tasks')
        foreach ($taskRow in $taskRows) {
            $effort = ConvertTo-SpecrewNullableDecimal -Value ([string]$taskRow.Effort)
            if ($null -ne $effort) {
                $sum += $effort
            }
        }
    }

    if ($sum -gt 0) {
        return $sum
    }

    $statePath = Join-Path $IterationDirectory 'state.md'
    if (@($StateLines).Count -eq 0 -and (Test-Path -LiteralPath $statePath -PathType Leaf)) {
        $StateLines = @(Get-SpecrewMarkdownContent -Path $statePath)
    }

    if (@($StateLines).Count -gt 0) {
        foreach ($line in @($StateLines)) {
            if ($line -match '^\|\s*\*\*Planned Story Points(?: \(Iteration \d+\))?\*\*\s*\|\s*(?<value>[^|]+)\|') {
                $planned = ConvertTo-SpecrewNullableDecimal -Value $Matches['value']
                if ($null -ne $planned) {
                    return $planned
                }
            }
        }
    }

    return $sum
}

function Get-SpecrewIterationRecord {
    param(
        [Parameter(Mandatory = $true)][string]$FeatureId,
        [Parameter(Mandatory = $true)][string]$FeatureTitle,
        [Parameter(Mandatory = $true)][string]$IterationDirectory,
        [AllowEmptyCollection()][object[]]$BoundaryCommits
    )

    $iterationName = Split-Path -Leaf $IterationDirectory
    $iterationLabel = Format-SpecrewIterationIdentifier -FeatureRef $FeatureId -IterationRef $iterationName
    $featureNumberValue = $null
    $featureNumberText = Get-SpecrewFeatureNumber -FeatureRef $FeatureId
    if (-not [int]::TryParse([string]$featureNumberText, [ref]$featureNumberValue)) {
        $featureNumberValue = $null
    }
    $iterationNumberValue = $null
    if (-not [int]::TryParse([string]$iterationName, [ref]$iterationNumberValue)) {
        $iterationNumberValue = $null
    }
    $statePath = Join-Path $IterationDirectory 'state.md'
    $planPath = Join-Path $IterationDirectory 'plan.md'
    $reviewPath = Join-Path $IterationDirectory 'review.md'
    $retroPath = Join-Path $IterationDirectory 'retro.md'

    if (-not (Test-Path -LiteralPath $statePath -PathType Leaf)) {
        $relatedLifecycleFiles = @(@($planPath, $reviewPath, $retroPath) | Where-Object { Test-Path -LiteralPath $_ -PathType Leaf })
        if ($relatedLifecycleFiles.Count -gt 0) {
            Add-SpecrewDashboardWarning -Message ("Skipping iteration artifact '{0}' because state.md is missing." -f $IterationDirectory)
        }
        return $null
    }

    try {
        $stateLines = @(Get-SpecrewMarkdownContent -Path $statePath)
        $planLines = if (Test-Path -LiteralPath $planPath -PathType Leaf) { @(Get-SpecrewMarkdownContent -Path $planPath) } else { @() }
    }
    catch {
        Add-SpecrewDashboardWarning -Message ("Skipping iteration artifact '{0}' because it could not be read cleanly: {1}" -f $IterationDirectory, $_.Exception.Message)
        return $null
    }

    $statePhase = [string](Get-SpecrewMarkdownMetadataValue -Lines $stateLines -Label 'Current Phase')
    $iterationStatus = [string](Get-SpecrewMarkdownMetadataValue -Lines $stateLines -Label 'Iteration Status')
    $reviewVerdict = ''
    $reviewLines = @()
    $retroLines = @()
    $startedAt = ConvertTo-SpecrewNullableDate -Value (Get-SpecrewMarkdownMetadataValue -Lines $planLines -Label 'Started')
    $completedAt = ConvertTo-SpecrewNullableDate -Value (Get-SpecrewMarkdownMetadataValue -Lines $planLines -Label 'Review Completed')

    $boundaryStartedAt = $null
    $boundaryCompletedAt = $null
    if ($BoundaryCommits.Count -gt 0 -and $null -ne $featureNumberValue -and $null -ne $iterationNumberValue) {
        $boundaryStartedAt = Get-SpecrewBoundaryCommitTimestamp -BoundaryCommits $BoundaryCommits -Boundary 'planning' -FeatureNumber $featureNumberValue -IterationNumber $iterationNumberValue
        $boundaryCompletedAt = Get-SpecrewBoundaryCommitTimestamp -BoundaryCommits $BoundaryCommits -Boundary 'iteration-closeout' -FeatureNumber $featureNumberValue -IterationNumber $iterationNumberValue -Latest
    }
    if ($null -ne $boundaryStartedAt) {
        $startedAt = $boundaryStartedAt
    }
    if ($null -ne $boundaryCompletedAt) {
        $completedAt = $boundaryCompletedAt
    }

    $isClosed = ($statePhase -match '(?i)closed|complete') -or ($iterationStatus -match '(?i)closed')
    if (-not $isClosed -and (Test-Path -LiteralPath $reviewPath -PathType Leaf)) {
        try {
            $reviewLines = @(Get-SpecrewMarkdownContent -Path $reviewPath)
            $reviewVerdict = [string](Get-SpecrewMarkdownMetadataValue -Lines $reviewLines -Label 'Overall Verdict')
            $isClosed = $reviewVerdict -match '(?i)accepted'
        }
        catch {
            Add-SpecrewDashboardWarning -Message ("Skipping review verdict for '{0}' because review.md could not be read cleanly: {1}" -f $IterationDirectory, $_.Exception.Message)
        }
    }

    if ($isClosed -and (Test-Path -LiteralPath $retroPath -PathType Leaf)) {
        try {
            $retroLines = @(Get-SpecrewMarkdownContent -Path $retroPath)
        }
        catch {
            Add-SpecrewDashboardWarning -Message ("Skipping retro evidence for '{0}' because retro.md could not be read cleanly: {1}" -f $IterationDirectory, $_.Exception.Message)
        }
    }

    if ($null -eq $completedAt -and @($retroLines).Count -gt 0) {
        $completedAt = ConvertTo-SpecrewNullableDate -Value (Get-SpecrewMarkdownMetadataValue -Lines $retroLines -Label 'Conducted At')
    }
    if ($null -eq $completedAt) {
        $completedAt = ConvertTo-SpecrewNullableDate -Value (Get-SpecrewMarkdownMetadataValue -Lines $stateLines -Label 'Updated')
    }

    $planned = Get-SpecrewPlannedStoryPoints -IterationDirectory $IterationDirectory -PlanLines $planLines -StateLines $stateLines
    $actual = if ($isClosed) { Get-SpecrewDeliveredStoryPoints -IterationDirectory $IterationDirectory -RetroLines $retroLines -StateLines $stateLines } else { [decimal]0 }
    if ($isClosed -and $actual -le 0 -and $planned -gt 0) {
        $actual = $planned
    }
    $elapsedDays = if ($null -ne $startedAt -and $null -ne $completedAt) {
        Get-SpecrewCalendarDayDuration -Start $startedAt -End $completedAt
    }
    else {
        1
    }

    return [pscustomobject]@{
        feature_ref           = $FeatureId
        feature_title         = $FeatureTitle
        iteration_ref         = $iterationName
        iteration_label       = $iterationLabel
        label                 = $iterationLabel
        display_name          = $iterationLabel
        iteration_directory   = $IterationDirectory
        planned_story_points  = [decimal]$planned
        actual_story_points   = [decimal]$actual
        delivered_story_points = [decimal]$actual
        started_at            = $startedAt
        closed_at             = if ($isClosed) { $completedAt } else { $null }
        elapsed_days          = $elapsedDays
        review_verdict        = $reviewVerdict
        state_phase           = $statePhase
        iteration_status      = $iterationStatus
        is_closed             = $isClosed
    }
}

function Get-SpecrewDerivedFeatureStatus {
    param(
        [AllowNull()][object]$ActiveIteration,
        [AllowEmptyCollection()][object[]]$ClosedIterations,
        [switch]$HasFeatureCloseout,
        [switch]$IsMainBranch,
        [switch]$IsActiveFeature
    )

    $canShip = if ($IsActiveFeature) { $HasFeatureCloseout -and $IsMainBranch } else { $true }

    if ($null -ne $ActiveIteration) {
        $phase = [string]$ActiveIteration.state_phase
        if (-not [string]::IsNullOrWhiteSpace($phase)) {
            $normalizedPhase = $phase.ToLowerInvariant()
            switch -Regex ($normalizedPhase) {
                'planning' { return 'Planning' }
                'review|demo' { return 'In Review' }
                'closed|complete' { return $(if ($canShip) { 'Shipped' } else { 'Iteration Complete' }) }
                default { return 'In Progress' }
            }
        }

        return 'In Progress'
    }

    if ($ClosedIterations.Count -gt 0) {
        return $(if ($canShip) { 'Shipped' } else { 'Implementation Complete' })
    }

    return 'Planning'
}

function Get-SpecrewFeatureRecords {
    param(
        [Parameter(Mandatory = $true)][string]$ProjectRoot,
        [AllowNull()][string]$ActiveFeatureRef
    )

    $specsPath = Join-Path $ProjectRoot 'specs'
    if (-not (Test-Path -LiteralPath $specsPath -PathType Container)) {
        return @()
    }

    $boundaryCommits = @(Get-SpecrewGitBoundaryCommits -ProjectRoot $ProjectRoot)
    $branchName = Get-SpecrewGitBranch -ProjectRoot $ProjectRoot
    $isMainBranch = $branchName -in @('main', 'master')

    $features = New-Object System.Collections.Generic.List[object]
    foreach ($featureDirectory in Get-ChildItem -LiteralPath $specsPath -Directory | Sort-Object Name) {
        $specPath = Join-Path $featureDirectory.FullName 'spec.md'
        if (-not (Test-Path -LiteralPath $specPath -PathType Leaf)) {
            continue
        }

        try {
            $specLines = @(Get-SpecrewMarkdownContent -Path $specPath)
        }
        catch {
            Add-SpecrewDashboardWarning -Message ("Skipping feature spec '{0}' because it could not be read cleanly: {1}" -f $specPath, $_.Exception.Message)
            continue
        }

        $featureTitle = Get-SpecrewMarkdownHeadingValue -Lines $specLines -Fallback $featureDirectory.Name
        $iterationRoot = Join-Path $featureDirectory.FullName 'iterations'
        $iterations = @()
        if (Test-Path -LiteralPath $iterationRoot -PathType Container) {
            $iterations = @(
                Get-ChildItem -LiteralPath $iterationRoot -Directory |
                    Where-Object { $_.Name -match '^\d+$' } |
                    Sort-Object Name |
                    ForEach-Object {
                        $iterationDirectoryPath = $_.FullName
                        try {
                            Get-SpecrewIterationRecord -FeatureId $featureDirectory.Name -FeatureTitle $featureTitle -IterationDirectory $iterationDirectoryPath -BoundaryCommits $boundaryCommits
                        }
                        catch {
                            Add-SpecrewDashboardWarning -Message ("Skipping iteration artifact '{0}' because it could not be parsed cleanly: {1}" -f $iterationDirectoryPath, $_.Exception.Message)
                            $null
                        }
                    } |
                    Where-Object { $null -ne $_ }
            )
        }

        $delivered = [decimal]0
        foreach ($iteration in @($iterations | Where-Object { $_.is_closed })) {
            $delivered += [decimal]$iteration.delivered_story_points
        }

        $activeIteration = @($iterations | Where-Object { -not $_.is_closed } | Sort-Object iteration_ref -Descending | Select-Object -First 1)
        $closedIterations = @($iterations | Where-Object { $_.is_closed } | Sort-Object closed_at, iteration_ref)
        $plannedTotal = [decimal]0
        foreach ($iteration in $iterations) {
            $plannedTotal += [decimal]$iteration.planned_story_points
        }
        $remainingTotal = [math]::Max(0, $plannedTotal - $delivered)
        $activeIterationValue = if ($activeIteration.Count -gt 0) { $activeIteration[0] } else { $null }
        $featureNumberValue = $null
        $featureNumberText = Get-SpecrewFeatureNumber -FeatureRef $featureDirectory.Name
        if (-not [int]::TryParse([string]$featureNumberText, [ref]$featureNumberValue)) {
            $featureNumberValue = $null
        }
        $hasFeatureCloseout = if ($null -ne $featureNumberValue) {
            $null -ne (Get-SpecrewBoundaryCommitTimestamp -BoundaryCommits $boundaryCommits -Boundary 'feature-closeout' -FeatureNumber $featureNumberValue -Latest)
        }
        else {
            $false
        }
        $isActiveFeature = -not [string]::IsNullOrWhiteSpace($ActiveFeatureRef) -and $featureDirectory.Name -eq $ActiveFeatureRef
        $featureStatus = Get-SpecrewDerivedFeatureStatus -ActiveIteration $activeIterationValue -ClosedIterations $closedIterations -HasFeatureCloseout:$hasFeatureCloseout -IsMainBranch:$isMainBranch -IsActiveFeature:$isActiveFeature
        $features.Add([pscustomobject]@{
                feature_ref             = $featureDirectory.Name
                feature_title           = $featureTitle
                feature_status          = $featureStatus
                has_feature_closeout    = $hasFeatureCloseout
                spec_path               = $specPath
                closeout_dashboard_path = Join-Path $featureDirectory.FullName 'closeout-dashboard.md'
                iterations              = @($iterations)
                closed_iterations       = @($closedIterations)
                active_iteration        = $activeIterationValue
                delivered_story_points  = $delivered
                planned_story_points    = [decimal]$plannedTotal
                remaining_story_points  = [decimal]$remainingTotal
            }) | Out-Null
    }

    return $features.ToArray()
}

function Read-SpecrewRoadmapDefinition {
    param([Parameter(Mandatory = $true)][string]$ProjectRoot)

    $roadmapPath = Join-Path $ProjectRoot '.specrew\roadmap.yml'
    if (-not (Test-Path -LiteralPath $roadmapPath -PathType Leaf)) {
        return [pscustomobject]@{
            path      = $roadmapPath
            exists    = $false
            phases    = @()
            warnings  = @()
            parse_ok  = $true
        }
    }

    $phases = New-Object System.Collections.Generic.List[object]
    $warnings = New-Object System.Collections.Generic.List[string]
    $lines = @(Get-SpecrewMarkdownContent -Path $roadmapPath)
    $current = $null
    $inFeatureRefs = $false
    foreach ($rawLine in $lines) {
        $line = $rawLine.TrimEnd()
        if ([string]::IsNullOrWhiteSpace($line) -or $line.TrimStart().StartsWith('#')) {
            continue
        }

        if ($line -match '^\s*phases:\s*$') {
            continue
        }

        if ($line -match '^\s*-\s+id:\s*(.+?)\s*$') {
            if ($null -ne $current) {
                $phases.Add([pscustomobject]$current) | Out-Null
            }

            $current = [ordered]@{
                id                = Get-SpecrewYamlScalarValue $Matches[1]
                name              = ''
                description       = ''
                planned_effort_sp = 0
                status            = 'queued'
                feature_refs      = New-Object System.Collections.Generic.List[string]
            }
            $inFeatureRefs = $false
            continue
        }

        if ($null -eq $current) {
            $warnings.Add("Ignoring roadmap content outside phases list: $line") | Out-Null
            continue
        }

        if ($line -match '^\s+name:\s*(.+?)\s*$') {
            $current.name = Get-SpecrewYamlScalarValue $Matches[1]
            $inFeatureRefs = $false
            continue
        }

        if ($line -match '^\s+description:\s*(.+?)\s*$') {
            $current.description = Get-SpecrewYamlScalarValue $Matches[1]
            $inFeatureRefs = $false
            continue
        }

        if ($line -match '^\s+planned_effort_sp:\s*(.+?)\s*$') {
            $value = ConvertTo-SpecrewNullableDecimal -Value (Get-SpecrewYamlScalarValue $Matches[1])
            $current.planned_effort_sp = if ($null -ne $value) { [int]$value } else { 0 }
            $inFeatureRefs = $false
            continue
        }

        if ($line -match '^\s+status:\s*(.+?)\s*$') {
            $current.status = (Get-SpecrewYamlScalarValue $Matches[1]).ToLowerInvariant()
            $inFeatureRefs = $false
            continue
        }

        if ($line -match '^\s+feature_refs:\s*$') {
            $inFeatureRefs = $true
            continue
        }

        if ($inFeatureRefs -and $line -match '^\s+-\s+(.+?)\s*$') {
            $current.feature_refs.Add((Get-SpecrewYamlScalarValue $Matches[1])) | Out-Null
            continue
        }
    }

    if ($null -ne $current) {
        $phases.Add([pscustomobject]$current) | Out-Null
    }

    if ($phases.Count -eq 0) {
        $warnings.Add('roadmap.yml does not declare any phases.') | Out-Null
    }

    return [pscustomobject]@{
        path      = $roadmapPath
        exists    = $true
        phases    = $phases.ToArray()
        warnings  = $warnings.ToArray()
        parse_ok  = $warnings.Count -eq 0
    }
}

function Get-SpecrewRoadmapProgress {
    param(
        [Parameter(Mandatory = $true)][object]$RoadmapDefinition,
        [Parameter(Mandatory = $true)][AllowEmptyCollection()][object[]]$FeatureRecords
    )

    $featureByRef = @{}
    foreach ($featureRecord in $FeatureRecords) {
        $featureByRef[$featureRecord.feature_ref] = $featureRecord
    }

    $warnings = New-Object System.Collections.Generic.List[string]
    $progress = New-Object System.Collections.Generic.List[object]
    $order = 0
    foreach ($phase in $RoadmapDefinition.phases) {
        $order++
        $derivedShipped = [decimal]0
        foreach ($featureRef in @($phase.feature_refs)) {
            if ($featureByRef.ContainsKey($featureRef)) {
                $derivedShipped += [decimal]$featureByRef[$featureRef].delivered_story_points
            }
            else {
                $warnings.Add("Roadmap phase '$($phase.name)' references missing feature '$featureRef'.") | Out-Null
            }
        }

        $remaining = [math]::Max(0, [decimal]$phase.planned_effort_sp - $derivedShipped)
        $overage = [math]::Max(0, $derivedShipped - [decimal]$phase.planned_effort_sp)
        $effectiveStatus = $phase.status
        if ($overage -gt 0) {
            $effectiveStatus = 'drifted-over'
            $warnings.Add("Roadmap drift: phase '$($phase.name)' shipped effort exceeds plan by $overage SP ($derivedShipped / $($phase.planned_effort_sp) SP).") | Out-Null
        }
        elseif (($derivedShipped -ge [decimal]$phase.planned_effort_sp -and $phase.planned_effort_sp -gt 0 -and $phase.status -ne 'shipped') -or
            ($derivedShipped -eq 0 -and $phase.status -eq 'shipped') -or
            ($derivedShipped -gt 0 -and $derivedShipped -lt [decimal]$phase.planned_effort_sp -and $phase.status -eq 'queued')) {
            $effectiveStatus = 'drifted'
            $warnings.Add("Roadmap drift: phase '$($phase.name)' declares '$($phase.status)' but derived shipped effort is $derivedShipped / $($phase.planned_effort_sp) SP.") | Out-Null
        }

        $progress.Add([pscustomobject]@{
                phase_id                  = $phase.id
                order                     = $order
                name                      = $phase.name
                description               = $phase.description
                planned_effort_sp         = [decimal]$phase.planned_effort_sp
                declared_status           = $phase.status
                effective_status          = $effectiveStatus
                feature_refs              = @($phase.feature_refs)
                derived_shipped_effort_sp = $derivedShipped
                remaining_effort_sp       = [decimal]$remaining
                overage_story_points      = [decimal]$overage
            }) | Out-Null
    }

    return [pscustomobject]@{
        phases   = $progress.ToArray()
        warnings = $warnings.ToArray()
    }
}

function Get-SpecrewDashboardDefaultRecentCount {
    return 6
}

function Get-SpecrewDashboardDefaultBarWidth {
    return 28
}

function Get-SpecrewDashboardCapabilityValue {
    param(
        [AllowNull()][object]$CapabilityOverrides,
        [Parameter(Mandatory = $true)][string]$Name,
        $DefaultValue
    )

    if ($null -eq $CapabilityOverrides) {
        return $DefaultValue
    }

    if ($CapabilityOverrides -is [System.Collections.IDictionary] -and $CapabilityOverrides.Contains($Name)) {
        return $CapabilityOverrides[$Name]
    }

    $property = $CapabilityOverrides.PSObject.Properties[$Name]
    if ($null -ne $property) {
        return $property.Value
    }

    return $DefaultValue
}

function Get-SpecrewConsoleEncodingName {
    try {
        return [Console]::OutputEncoding.WebName
    }
    catch {
        return ''
    }
}

function Get-SpecrewIsWindowsHost {
    param([AllowNull()][object]$CapabilityOverrides)

    $defaultValue = $false
    if ($PSVersionTable.PSVersion.Major -ge 6) {
        $defaultValue = [bool]$IsWindows
    }

    return [bool](Get-SpecrewDashboardCapabilityValue -CapabilityOverrides $CapabilityOverrides -Name 'IsWindows' -DefaultValue $defaultValue)
}

function Test-SpecrewUtf8Output {
    param(
        [AllowNull()][object]$CapabilityOverrides,
        [ValidateSet('live', 'iteration-closeout', 'feature-closeout')][string]$CaptureKind = 'live'
    )

    if ($CaptureKind -ne 'live') {
        return $true
    }

    $encodingName = [string](Get-SpecrewDashboardCapabilityValue -CapabilityOverrides $CapabilityOverrides -Name 'ConsoleEncodingName' -DefaultValue (Get-SpecrewConsoleEncodingName))
    if ($encodingName -match 'utf-?8') {
        return $true
    }

    if (-not (Get-SpecrewIsWindowsHost -CapabilityOverrides $CapabilityOverrides)) {
        $language = [string](Get-SpecrewDashboardCapabilityValue -CapabilityOverrides $CapabilityOverrides -Name 'Lang' -DefaultValue $(if (-not [string]::IsNullOrWhiteSpace($env:LC_ALL)) { $env:LC_ALL } elseif (-not [string]::IsNullOrWhiteSpace($env:LC_CTYPE)) { $env:LC_CTYPE } else { $env:LANG }))
        if ($language -match 'utf-?8') {
            return $true
        }
    }

    return $false
}

function Test-SpecrewVirtualTerminalSupport {
    param(
        [AllowNull()][object]$CapabilityOverrides,
        [ValidateSet('live', 'iteration-closeout', 'feature-closeout')][string]$CaptureKind = 'live'
    )

    if ($CaptureKind -ne 'live') {
        return $false
    }

    if (-not (Get-SpecrewIsWindowsHost -CapabilityOverrides $CapabilityOverrides)) {
        return $true
    }

    $override = Get-SpecrewDashboardCapabilityValue -CapabilityOverrides $CapabilityOverrides -Name 'SupportsVirtualTerminal' -DefaultValue $null
    if ($null -ne $override) {
        return [bool]$override
    }

    try {
        if ($null -ne $Host.UI -and $Host.UI.PSObject.Properties.Match('SupportsVirtualTerminal').Count -gt 0) {
            return [bool]$Host.UI.SupportsVirtualTerminal
        }
    }
    catch {
        return $false
    }

    return $false
}

function Get-SpecrewDashboardRenderProfile {
    param(
        [switch]$Ascii,
        [switch]$NoColor,
        [int]$RecentCount = 6,
        [int]$BarWidth = 28,
        [ValidateSet('live', 'iteration-closeout', 'feature-closeout')][string]$CaptureKind = 'live',
        [AllowNull()][object]$CapabilityOverrides
    )

    $effectiveRecentCount = if ($RecentCount -gt 0) { $RecentCount } else { Get-SpecrewDashboardDefaultRecentCount }
    $effectiveBarWidth = if ($BarWidth -gt 0) { $BarWidth } else { Get-SpecrewDashboardDefaultBarWidth }
    $isWindowsHost = Get-SpecrewIsWindowsHost -CapabilityOverrides $CapabilityOverrides
    $termValue = [string](Get-SpecrewDashboardCapabilityValue -CapabilityOverrides $CapabilityOverrides -Name 'Term' -DefaultValue $env:TERM)
    $utf8Eligible = Test-SpecrewUtf8Output -CapabilityOverrides $CapabilityOverrides -CaptureKind $CaptureKind
    $virtualTerminalSupported = Test-SpecrewVirtualTerminalSupport -CapabilityOverrides $CapabilityOverrides -CaptureKind $CaptureKind
    $fallbackReason = $null

    if ($Ascii) {
        $fallbackReason = 'ASCII rendering forced by --ASCII.'
    }
    elseif ($NoColor -or -not [string]::IsNullOrWhiteSpace($env:NO_COLOR)) {
        $fallbackReason = 'Monochrome-safe fallback forced by --no-color / NO_COLOR.'
    }
    elseif (-not [string]::IsNullOrWhiteSpace($env:NO_UNICODE)) {
        $fallbackReason = 'Unicode rendering disabled by NO_UNICODE.'
    }
    elseif ($CaptureKind -eq 'live' -and $termValue -eq 'dumb') {
        $fallbackReason = 'TERM=dumb disables rich terminal rendering.'
    }
    elseif (-not $utf8Eligible) {
        $fallbackReason = 'UTF-8-capable output is unavailable.'
    }
    elseif ($CaptureKind -eq 'live' -and $isWindowsHost -and -not $virtualTerminalSupported) {
        $fallbackReason = 'Windows virtual-terminal support is unavailable.'
    }

    $renderingMode = if ([string]::IsNullOrWhiteSpace($fallbackReason)) { 'rich' } else { 'monochrome' }
    $ansiEnabled = $renderingMode -eq 'rich' -and $CaptureKind -eq 'live' -and $virtualTerminalSupported

    return [pscustomobject]@{
        rendering_mode            = $renderingMode
        ansi_enabled              = $ansiEnabled
        unicode_enabled           = $renderingMode -eq 'rich'
        ascii_forced              = $Ascii.IsPresent
        fallback_reason           = $fallbackReason
        recent_count              = $effectiveRecentCount
        bar_width                 = $effectiveBarWidth
        snapshot_strip_ansi       = $CaptureKind -ne 'live'
        capture_kind              = $CaptureKind
        color_mode                = if ($ansiEnabled) { 'semantic-color' } else { 'monochrome' }
        utf8_eligible             = $utf8Eligible
        supports_virtual_terminal = $virtualTerminalSupported
        term                      = $termValue
        is_windows                = $isWindowsHost
    }
}

function Get-SpecrewDashboardColorMode {
    param(
        [switch]$Ascii,
        [switch]$NoColor,
        [ValidateSet('live', 'iteration-closeout', 'feature-closeout')][string]$CaptureKind = 'live',
        [AllowNull()][object]$CapabilityOverrides
    )

    return (Get-SpecrewDashboardRenderProfile -Ascii:$Ascii -NoColor:$NoColor -CaptureKind $CaptureKind -CapabilityOverrides $CapabilityOverrides).color_mode
}

function Get-SpecrewDashboardGlyphPalette {
    param([Parameter(Mandatory = $true)][object]$RenderProfile)

    if ($RenderProfile.rendering_mode -eq 'rich') {
        return [pscustomobject]@{
            RuleChar      = '─'
            ActiveArrow   = '→'
            ActiveMarker  = '◐'
            ShippedMarker = '✓'
            QueuedMarker  = '○'
            WarnMarker    = '⚠'
            BarFill       = '█'
            BarEmpty      = '░'
            ProgressFill  = '█'
            ProgressEmpty = '░'
            SparkChars    = @('▁', '▂', '▃', '▄', '▅', '▆', '▇', '█')
            FooterMarker  = 'ℹ'
        }
    }

    return [pscustomobject]@{
        RuleChar      = '-'
        ActiveArrow   = '>'
        ActiveMarker  = '[~]'
        ShippedMarker = '[x]'
        QueuedMarker  = '[ ]'
        WarnMarker    = '!'
        BarFill       = '#'
        BarEmpty      = '.'
        ProgressFill  = '#'
        ProgressEmpty = '.'
        SparkChars    = @()
        FooterMarker  = 'i'
    }
}

function Format-SpecrewDashboardMeter {
    param(
        [decimal]$Value,
        [decimal]$Maximum,
        [int]$Width,
        [Parameter(Mandatory = $true)][object]$RenderProfile
    )

    $palette = Get-SpecrewDashboardGlyphPalette -RenderProfile $RenderProfile
    $safeWidth = [math]::Max(1, $Width)
    $filled = if ($Maximum -gt 0) {
        [math]::Min($safeWidth, [int][math]::Round(($Value / $Maximum) * $safeWidth))
    }
    else {
        0
    }
    $filled = [math]::Max(0, $filled)

    return ('{0}{1}' -f ($palette.BarFill * $filled), ($palette.BarEmpty * ($safeWidth - $filled)))
}

function Format-SpecrewDashboardProgressBar {
    param(
        [decimal]$Current,
        [decimal]$Total,
        [int]$Width = 16,
        [string]$OverflowMarker = ' ',
        [Parameter(Mandatory = $true)][object]$RenderProfile
    )

    $palette = Get-SpecrewDashboardGlyphPalette -RenderProfile $RenderProfile
    $safeWidth = [math]::Max(1, $Width)
    $percent = if ($Total -gt 0) { [math]::Round(($Current / $Total) * 100) } else { 0 }
    $ratio = if ($Total -gt 0) { $Current / $Total } else { 0 }
    $filled = [math]::Min($safeWidth, [int][math]::Round($ratio * $safeWidth))
    $safeOverflowMarker = if ([string]::IsNullOrEmpty($OverflowMarker)) { ' ' } else { $OverflowMarker.Substring(0, 1) }
    return ('[{0}{1}]{2} {3,3}%' -f ($palette.ProgressFill * $filled), ($palette.ProgressEmpty * ($safeWidth - $filled)), $safeOverflowMarker, $percent)
}

function Format-SpecrewDashboardBarRow {
    param(
        [Parameter(Mandatory = $true)][string]$Label,
        [decimal]$Value,
        [decimal]$Maximum,
        [int]$Width = 14,
        [Parameter(Mandatory = $true)][object]$RenderProfile
    )

    $meter = Format-SpecrewDashboardMeter -Value $Value -Maximum $Maximum -Width $Width -RenderProfile $RenderProfile
    return ('{0,-18} {1,5:0.#} SP {2}' -f $Label, $Value, $meter)
}

function Get-SpecrewVelocityHeadline {
    param(
        [AllowEmptyCollection()]
        [Parameter(Mandatory = $true)][object[]]$ClosedIterations
    )

    $recent = @($ClosedIterations | Sort-Object closed_at -Descending | Select-Object -First 10)
    if ($recent.Count -eq 0) {
        return [pscustomobject]@{
            sample_size                  = 0
            total_story_points           = [decimal]0
            total_elapsed_days           = [decimal]0
            average_elapsed_days         = [decimal]0
            points_per_day               = [decimal]0
            confidence                   = 'low'
            trend_tokens                 = ''
            recent_values                = @()
            sample_basis_text            = 'Awaiting the first closed iteration before velocity can be summarized.'
            insufficient_history_message = 'Need at least one closed iteration before velocity can be calculated.'
            iterations                   = @()
        }
    }

    $totalStoryPoints = [decimal]0
    $totalDays = [decimal]0
    $tokens = New-Object System.Collections.Generic.List[string]
    $recentValues = New-Object System.Collections.Generic.List[decimal]
    foreach ($iteration in $recent) {
        $storyPoints = [decimal]$iteration.actual_story_points
        $elapsedDays = [decimal]([math]::Max(1, $iteration.elapsed_days))
        $totalStoryPoints += $storyPoints
        $totalDays += $elapsedDays
        $tokens.Add(('{0:0.#}' -f $storyPoints)) | Out-Null
        $recentValues.Add($storyPoints) | Out-Null
    }

    $pointsPerDay = if ($totalDays -gt 0) { [math]::Round(($totalStoryPoints / $totalDays), 2) } else { [decimal]0 }
    $averageDays = if ($recent.Count -gt 0) { [math]::Round(($totalDays / $recent.Count), 1) } else { [decimal]0 }
    $confidence = if ($recent.Count -ge 10) { 'high' } elseif ($recent.Count -ge 4) { 'moderate' } else { 'low' }
    $insufficientHistoryMessage = if ($recent.Count -lt 2) {
        'Need at least two closed iterations before the velocity trend can show a stable direction.'
    }
    else {
        $null
    }

    return [pscustomobject]@{
        sample_size                  = $recent.Count
        total_story_points           = $totalStoryPoints
        total_elapsed_days           = $totalDays
        average_elapsed_days         = $averageDays
        points_per_day               = $pointsPerDay
        confidence                   = $confidence
        trend_tokens                 = ($tokens -join ' / ')
        recent_values                = $recentValues.ToArray()
        sample_basis_text            = ('Based on {0} closed iteration(s), {1:0.#} SP across {2:0.#} calendar day(s) (avg {3:0.#} day(s)).' -f $recent.Count, $totalStoryPoints, $totalDays, $averageDays)
        insufficient_history_message = $insufficientHistoryMessage
        iterations                   = $recent
    }
}

function Get-SpecrewEtaText {
    param(
        [AllowNull()][object]$Remaining,
        [decimal]$PointsPerDay,
        [string]$CompletedLabel = 'shipped'
    )

    if ($null -eq $Remaining) {
        return 'TBD'
    }

    $remainingValue = [decimal]$Remaining

    if ($remainingValue -le 0) {
        return $CompletedLabel
    }

    if ($PointsPerDay -le 0) {
        return 'TBD'
    }

    $etaDays = [math]::Ceiling($remainingValue / $PointsPerDay)
    return "$etaDays calendar day(s)"
}

function Resolve-SpecrewEtaCompletionLabel {
    param(
        [AllowNull()][string]$Status,
        [string]$Default = 'in-progress'
    )

    if ([string]::IsNullOrWhiteSpace($Status)) {
        return $Default
    }

    $normalized = $Status.ToLowerInvariant()
    switch ($normalized) {
        'shipped' { return 'shipped' }
        'queued' { return 'queued' }
        'in-progress' { return 'in-progress' }
        'drifted' { return 'in-progress' }
        'drifted-over' { return 'in-progress' }
        default { return $Default }
    }
}

function Get-SpecrewFeatureDisplayCode {
    param([AllowNull()][string]$FeatureRef)

    $featureNumber = Get-SpecrewFeatureNumber -FeatureRef $FeatureRef
    if ([string]::IsNullOrWhiteSpace($featureNumber)) {
        return $FeatureRef
    }

    return ('F-{0}' -f $featureNumber)
}

function Get-SpecrewFeatureReadableTitle {
    param(
        [AllowNull()][string]$FeatureTitle,
        [AllowNull()][string]$FeatureRef
    )

    $title = if (-not [string]::IsNullOrWhiteSpace($FeatureTitle)) {
        $FeatureTitle -replace '^Feature Specification:\s*', ''
    }
    else {
        $FeatureRef
    }

    return $title.Trim()
}

function Format-SpecrewDashboardTruncatedText {
    param(
        [AllowNull()][string]$Text,
        [int]$MaxWidth
    )

    if ($MaxWidth -le 0) {
        return ''
    }

    if ([string]::IsNullOrWhiteSpace($Text)) {
        return ''
    }

    $trimmed = $Text.Trim()
    if ($trimmed.Length -le $MaxWidth) {
        return $trimmed
    }

    if ($MaxWidth -le 3) {
        return $trimmed.Substring(0, $MaxWidth)
    }

    return ($trimmed.Substring(0, $MaxWidth - 3) + '...')
}

function Get-SpecrewRecentShippedLabel {
    param(
        [AllowNull()][string]$FeatureRef,
        [AllowNull()][string]$IterationLabel
    )

    $featureCode = Get-SpecrewFeatureDisplayCode -FeatureRef $FeatureRef
    $iterationToken = $IterationLabel
    if (-not [string]::IsNullOrWhiteSpace($IterationLabel) -and $IterationLabel -match '(iter-\d+)$') {
        $iterationToken = $Matches[1]
    }

    if (-not [string]::IsNullOrWhiteSpace($featureCode) -and -not [string]::IsNullOrWhiteSpace($iterationToken)) {
        return ('{0} · {1}' -f $featureCode, $iterationToken)
    }

    if (-not [string]::IsNullOrWhiteSpace($featureCode)) {
        return $featureCode
    }

    return $iterationToken
}

function Get-SpecrewRecentShippedIterationToken {
    param(
        [AllowNull()][string]$IterationLabel,
        [AllowNull()][string]$IterationRef
    )

    $iterationToken = $IterationRef
    if ([string]::IsNullOrWhiteSpace($iterationToken) -and -not [string]::IsNullOrWhiteSpace($IterationLabel) -and $IterationLabel -match '(iter-\d+)$') {
        $iterationToken = $Matches[1]
    }
    elseif (-not [string]::IsNullOrWhiteSpace($iterationToken) -and $iterationToken -notmatch '^iter-') {
        $iterationToken = "iter-$iterationToken"
    }

    if ([string]::IsNullOrWhiteSpace($iterationToken)) {
        return ''
    }

    return ('· {0}' -f $iterationToken)
}

function New-SpecrewRecentShippedEntry {
    param(
        [Parameter(Mandatory = $true)][object]$Iteration,
        [AllowNull()][object]$FeatureRecord
    )

    $closeDateText = if ($null -ne $Iteration.closed_at) { $Iteration.closed_at.ToString('yyyy-MM-dd') } else { 'unknown-date' }
    $closedIterationCount = if ($null -ne $FeatureRecord) { @($FeatureRecord.closed_iterations).Count } else { 1 }

    return [pscustomobject]@{
        feature_ref            = $Iteration.feature_ref
        feature_code           = Get-SpecrewFeatureDisplayCode -FeatureRef $Iteration.feature_ref
        label                  = Get-SpecrewRecentShippedLabel -FeatureRef $Iteration.feature_ref -IterationLabel $Iteration.iteration_label
        short_name             = Get-SpecrewFeatureReadableTitle -FeatureTitle $Iteration.feature_title -FeatureRef $Iteration.feature_ref
        iteration_label        = $Iteration.iteration_label
        delivered_story_points = [decimal]$Iteration.actual_story_points
        iteration_count        = [int]$closedIterationCount
        close_date_text        = $closeDateText
        closed_at              = $Iteration.closed_at
        actual_story_points    = [decimal]$Iteration.actual_story_points
        planned_story_points   = [decimal]$Iteration.planned_story_points
        elapsed_days           = [int]$Iteration.elapsed_days
        feature_title          = $Iteration.feature_title
        iteration_ref          = $Iteration.iteration_ref
        feature_status         = $Iteration.iteration_status
    }
}

function Get-SpecrewVelocitySparkline {
    param(
        [AllowEmptyCollection()][decimal[]]$Values,
        [Parameter(Mandatory = $true)][object]$RenderProfile
    )

    if ($RenderProfile.rendering_mode -ne 'rich' -or $Values.Count -eq 0) {
        return $null
    }

    $palette = Get-SpecrewDashboardGlyphPalette -RenderProfile $RenderProfile
    if ($palette.SparkChars.Count -eq 0) {
        return $null
    }

    $minimum = ($Values | Measure-Object -Minimum).Minimum
    $maximum = ($Values | Measure-Object -Maximum).Maximum
    $spark = New-Object System.Collections.Generic.List[string]

    foreach ($value in $Values) {
        if ($maximum -eq $minimum) {
            $index = [math]::Floor(($palette.SparkChars.Count - 1) / 2)
        }
        else {
            $ratio = ([decimal]$value - [decimal]$minimum) / ([decimal]$maximum - [decimal]$minimum)
            $index = [math]::Min($palette.SparkChars.Count - 1, [int][math]::Round($ratio * ($palette.SparkChars.Count - 1)))
        }
        $spark.Add($palette.SparkChars[$index]) | Out-Null
    }

    return ($spark -join '')
}

function Format-SpecrewRoadmapDescription {
    param([AllowNull()][string]$Description)

    if ([string]::IsNullOrWhiteSpace($Description)) {
        return 'No roadmap description recorded.'
    }

    $trimmed = $Description.Trim()
    if ($trimmed.Length -le 80) {
        return $trimmed
    }

    return ($trimmed.Substring(0, 77) + '...')
}

function Get-SpecrewProjection {
    param(
        [Parameter(Mandatory = $true)][object]$VelocityHeadline,
        [AllowEmptyCollection()]
        [Parameter(Mandatory = $true)][object[]]$RoadmapPhases,
        [AllowNull()][object]$ActiveFeature,
        [AllowNull()][object]$ActivePhase
    )

    $pointsPerDay = [decimal]$VelocityHeadline.points_per_day
    $roadmapRemaining = if ($RoadmapPhases.Count -gt 0) {
        $total = [decimal]0
        foreach ($phase in $RoadmapPhases) {
            $total += [decimal]$phase.remaining_effort_sp
        }
        $total
    }
    else {
        $null
    }

    $activeFeatureRemaining = $null
    if ($null -ne $ActiveFeature -and $ActiveFeature.planned_story_points -gt 0) {
        $activeFeatureRemaining = [decimal]$ActiveFeature.remaining_story_points
        if ($activeFeatureRemaining -le 0 -and $ActiveFeature.delivered_story_points -le 0) {
            $activeFeatureRemaining = $null
        }
        if ($ActiveFeature.feature_status -eq 'Planning') {
            $activeFeatureRemaining = $null
        }
    }
    $activePhaseRemaining = if ($null -ne $ActivePhase) { [decimal]$ActivePhase.remaining_effort_sp } else { $null }
    $featureCompletedLabel = 'shipped'
    if ($null -ne $ActiveFeature -and -not [string]::IsNullOrWhiteSpace([string]$ActiveFeature.feature_status)) {
        $statusText = [string]$ActiveFeature.feature_status
        switch -Regex ($statusText) {
            'implementation complete' { $featureCompletedLabel = 'implementation complete' }
            'iteration complete' { $featureCompletedLabel = 'iteration complete' }
            'shipped' { $featureCompletedLabel = 'shipped' }
            default { $featureCompletedLabel = 'TBD' }
        }
    }

    $phaseCompletedLabel = 'in-progress'
    if ($null -ne $ActivePhase) {
        $phaseStatus = if (-not [string]::IsNullOrWhiteSpace([string]$ActivePhase.effective_status)) {
            [string]$ActivePhase.effective_status
        }
        elseif (-not [string]::IsNullOrWhiteSpace([string]$ActivePhase.declared_status)) {
            [string]$ActivePhase.declared_status
        }
        else {
            $null
        }
        $phaseCompletedLabel = Resolve-SpecrewEtaCompletionLabel -Status $phaseStatus -Default 'in-progress'
    }

    $roadmapCompletedLabel = 'in-progress'
    if ($RoadmapPhases.Count -gt 0) {
        $normalizedStatuses = @()
        foreach ($phase in $RoadmapPhases) {
            $phaseStatus = if (-not [string]::IsNullOrWhiteSpace([string]$phase.effective_status)) {
                [string]$phase.effective_status
            }
            elseif (-not [string]::IsNullOrWhiteSpace([string]$phase.declared_status)) {
                [string]$phase.declared_status
            }
            else {
                $null
            }
            if (-not [string]::IsNullOrWhiteSpace($phaseStatus)) {
                $normalizedStatuses += $phaseStatus.ToLowerInvariant()
            }
        }

        if ($normalizedStatuses.Count -gt 0 -and -not ($normalizedStatuses | Where-Object { $_ -ne 'shipped' } | Select-Object -First 1)) {
            $roadmapCompletedLabel = 'shipped'
        }
        elseif ($normalizedStatuses.Count -gt 0 -and -not ($normalizedStatuses | Where-Object { $_ -ne 'queued' } | Select-Object -First 1)) {
            $roadmapCompletedLabel = 'queued'
        }
    }

    $etaScopes = @(
        [pscustomobject]@{
            scope_id              = 'active-feature'
            label                 = 'Active feature'
            remaining_story_points = $activeFeatureRemaining
            eta_text              = Get-SpecrewEtaText -Remaining $activeFeatureRemaining -PointsPerDay $pointsPerDay -CompletedLabel $featureCompletedLabel
            confidence            = $VelocityHeadline.confidence
        },
        [pscustomobject]@{
            scope_id              = 'current-phase'
            label                 = 'Current phase'
            remaining_story_points = $activePhaseRemaining
            eta_text              = Get-SpecrewEtaText -Remaining $activePhaseRemaining -PointsPerDay $pointsPerDay -CompletedLabel $phaseCompletedLabel
            confidence            = $VelocityHeadline.confidence
        },
        [pscustomobject]@{
            scope_id              = 'roadmap'
            label                 = 'Roadmap'
            remaining_story_points = $roadmapRemaining
            eta_text              = Get-SpecrewEtaText -Remaining $roadmapRemaining -PointsPerDay $pointsPerDay -CompletedLabel $roadmapCompletedLabel
            confidence            = $VelocityHeadline.confidence
        }
    )

    return [pscustomobject]@{
            remaining_story_points = $roadmapRemaining
            eta_text               = Get-SpecrewEtaText -Remaining $roadmapRemaining -PointsPerDay $pointsPerDay -CompletedLabel $roadmapCompletedLabel
        confidence             = $VelocityHeadline.confidence
        eta_scopes             = @($etaScopes)
    }
}

function Get-SpecrewDashboardSnapshot {
    param(
        [Parameter(Mandatory = $true)][string]$ProjectRoot,
        [AllowNull()][string]$FeatureId,
        [AllowNull()][string]$IterationNumber,
        [switch]$Compact,
        [switch]$Ascii,
        [switch]$NoColor,
        [int]$RecentCount = 6,
        [int]$BarWidth = 28,
        [ValidateSet('live', 'iteration-closeout', 'feature-closeout')][string]$CaptureKind = 'live',
        [AllowNull()][object]$CapabilityOverrides,
        [switch]$Team
    )

    $resolvedProjectRoot = (Resolve-Path -Path (Resolve-ProjectPath -Path $ProjectRoot)).Path
    Reset-SpecrewDashboardWarnings
    $renderProfile = Get-SpecrewDashboardRenderProfile -Ascii:$Ascii -NoColor:$NoColor -RecentCount $RecentCount -BarWidth $BarWidth -CaptureKind $CaptureKind -CapabilityOverrides $CapabilityOverrides
    $activeFeatureRef = $null
    if (-not [string]::IsNullOrWhiteSpace($FeatureId)) {
        $activeFeatureRef = $FeatureId
    }
    else {
        $activeFeatureDirectory = Get-SpecrewActiveFeatureDirectory -ProjectRoot $resolvedProjectRoot
        $activeFeatureRef = if ($activeFeatureDirectory) { Split-Path -Leaf $activeFeatureDirectory } else { $null }
    }
    $features = @(Get-SpecrewFeatureRecords -ProjectRoot $resolvedProjectRoot -ActiveFeatureRef $activeFeatureRef)
    $featureLookup = @{}
    foreach ($feature in $features) {
        $featureLookup[$feature.feature_ref] = $feature
    }

    $warnings = New-Object System.Collections.Generic.List[string]
    if ($Team) {
        $warnings.Add('Team mode is reserved for future multi-developer support; rendering the personal dashboard instead.') | Out-Null
    }
    if ($renderProfile.rendering_mode -eq 'monochrome' -and -not [string]::IsNullOrWhiteSpace([string]$renderProfile.fallback_reason)) {
        $warnings.Add($renderProfile.fallback_reason) | Out-Null
    }

    $featureRecord = @($features | Where-Object { $_.feature_ref -eq $activeFeatureRef } | Select-Object -First 1)
    if ($featureRecord.Count -eq 0 -and $features.Count -gt 0) {
        $featureRecord = @($features | Select-Object -First 1)
    }
    $featureRecord = if ($featureRecord.Count -gt 0) { $featureRecord[0] } else { $null }

    $closedIterations = @(
        $features |
            ForEach-Object { $_.closed_iterations } |
            Where-Object { $null -ne $_ } |
            Sort-Object closed_at -Descending
    )
    $velocityHeadline = Get-SpecrewVelocityHeadline -ClosedIterations $closedIterations

    if ($closedIterations.Count -eq 0) {
        $warnings.Add('No closed iterations yet. Velocity and shipped sections will stay in empty-state mode until the first closeout lands.') | Out-Null
    }
    elseif ($closedIterations.Count -le 3) {
        $warnings.Add("Velocity uses only $($closedIterations.Count) closed iteration(s); confidence remains low until 4+ iterations are available.") | Out-Null
    }

    $roadmapDefinition = Read-SpecrewRoadmapDefinition -ProjectRoot $resolvedProjectRoot
    if (-not $roadmapDefinition.exists) {
        $warnings.Add('No .specrew/roadmap.yml file found. Add one to enable roadmap progress and remaining-effort projection (see docs/roadmap-maintenance.md).') | Out-Null
    }
    foreach ($warning in $roadmapDefinition.warnings) {
        $warnings.Add($warning) | Out-Null
    }

    $roadmapProgress = Get-SpecrewRoadmapProgress -RoadmapDefinition $roadmapDefinition -FeatureRecords $features
    foreach ($warning in $roadmapProgress.warnings) {
        $warnings.Add($warning) | Out-Null
    }
    foreach ($warning in Get-SpecrewDashboardWarnings) {
        $warnings.Add($warning) | Out-Null
    }

    $activePhase = $null
    if ($null -ne $featureRecord -and $roadmapProgress.phases.Count -gt 0) {
        $activePhase = @($roadmapProgress.phases | Where-Object { $_.feature_refs -contains $featureRecord.feature_ref } | Sort-Object order | Select-Object -First 1)
        if ($activePhase.Count -gt 0) {
            $activePhase = $activePhase[0]
        }
        else {
            $activePhase = $null
        }
    }

    $activeIteration = $null
    if ($null -ne $featureRecord) {
        if (-not [string]::IsNullOrWhiteSpace($IterationNumber)) {
            $activeIteration = @($featureRecord.iterations | Where-Object { $_.iteration_ref -eq $IterationNumber } | Select-Object -First 1)
            if ($activeIteration.Count -gt 0) {
                $activeIteration = $activeIteration[0]
            }
            else {
                $activeIteration = $null
                $warnings.Add("Requested iteration '$IterationNumber' was not found under feature '$($featureRecord.feature_ref)'.") | Out-Null
            }
        }

        if ($null -eq $activeIteration) {
            $activeIteration = $featureRecord.active_iteration
        }
    }

    if ($null -eq $featureRecord) {
        $warnings.Add('No feature specifications were found under specs/.') | Out-Null
    }
    elseif ($null -eq $activeIteration) {
        $warnings.Add("Feature '$($featureRecord.feature_ref)' has no active iteration artifact; showing feature-level context only.") | Out-Null
    }

    $projection = Get-SpecrewProjection -VelocityHeadline $velocityHeadline -RoadmapPhases $roadmapProgress.phases -ActiveFeature $featureRecord -ActivePhase $activePhase
    $recentShipped = @(
        $closedIterations |
            Select-Object -First $renderProfile.recent_count |
            ForEach-Object { New-SpecrewRecentShippedEntry -Iteration $_ -FeatureRecord $featureLookup[$_.feature_ref] }
    )
    $snapshot = [pscustomobject]@{
        schema_version       = 'v1'
        captured_at          = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')
        today_anchor         = (Get-Date).ToString('yyyy-MM-dd')
        render_mode          = if ($Compact) { 'compact' } else { 'full' }
        color_mode           = $renderProfile.color_mode
        render_profile       = $renderProfile
        repository_identity  = [pscustomobject]@{
            name   = Split-Path -Leaf $resolvedProjectRoot
            branch = Get-SpecrewGitBranch -ProjectRoot $resolvedProjectRoot
            root   = $resolvedProjectRoot
        }
        active_feature       = $featureRecord
        active_iteration     = $activeIteration
        active_phase         = $activePhase
        summary_line         = ''
        velocity_headline    = $velocityHeadline
        recent_shipped       = @($recentShipped)
        recent_variance      = @($closedIterations | Select-Object -First 3)
        history              = @($closedIterations | Select-Object -First 8)
        roadmap_progress     = @($roadmapProgress.phases)
        projection           = $projection
        footer_note          = ''
        warnings             = @($warnings | Select-Object -Unique)
    }
    $snapshot.summary_line = Get-SpecrewSummaryLine -Snapshot $snapshot -Compact:$Compact
    $snapshot.footer_note = if ($renderProfile.rendering_mode -eq 'rich') {
        'Use --ASCII any time you need the monochrome-safe fallback; stored closeout snapshots keep Unicode glyphs but never ANSI escapes.'
    }
    else {
        'Monochrome-safe fallback is active. Re-run without --ASCII / --no-color in a UTF-8 + ANSI-capable terminal to see the richer view.'
    }
    return $snapshot
}

function Get-SpecrewSummaryLine {
    param(
        [Parameter(Mandatory = $true)][object]$Snapshot,
        [switch]$Compact
    )

    $palette = Get-SpecrewDashboardGlyphPalette -RenderProfile $Snapshot.render_profile
    $featureLabel = if ($null -ne $Snapshot.active_feature) {
        '{0} {1}' -f (Get-SpecrewFeatureDisplayCode -FeatureRef $Snapshot.active_feature.feature_ref), (Get-SpecrewFeatureReadableTitle -FeatureTitle $Snapshot.active_feature.feature_title -FeatureRef $Snapshot.active_feature.feature_ref)
    }
    else {
        'No active feature'
    }

    if ($null -ne $Snapshot.active_feature) {
        $featureLabel = '{0} {1}' -f $palette.ActiveArrow, $featureLabel
    }

    $statusText = if ($null -ne $Snapshot.active_feature -and -not [string]::IsNullOrWhiteSpace([string]$Snapshot.active_feature.feature_status)) {
        $Snapshot.active_feature.feature_status
    }
    else {
        'Unknown'
    }

    $phaseText = if ($null -ne $Snapshot.active_iteration -and -not [string]::IsNullOrWhiteSpace([string]$Snapshot.active_iteration.state_phase)) {
        $Snapshot.active_iteration.state_phase.ToLowerInvariant()
    }
    else {
        $null
    }

    $velocityText = if ($Snapshot.velocity_headline.sample_size -gt 0) {
        '{0:0.##} SP/day ({1} closed iterations, {2})' -f $Snapshot.velocity_headline.points_per_day, $Snapshot.velocity_headline.sample_size, $Snapshot.velocity_headline.confidence
    }
    else {
        'Velocity TBD'
    }

    $etaScopes = @($Snapshot.projection.eta_scopes)
    $featureEta = (@($etaScopes | Where-Object { $_.scope_id -eq 'active-feature' } | Select-Object -First 1).eta_text | Select-Object -First 1)
    $phaseEta = (@($etaScopes | Where-Object { $_.scope_id -eq 'current-phase' } | Select-Object -First 1).eta_text | Select-Object -First 1)
    $roadmapEta = (@($etaScopes | Where-Object { $_.scope_id -eq 'roadmap' } | Select-Object -First 1).eta_text | Select-Object -First 1)
    if ([string]::IsNullOrWhiteSpace($featureEta)) { $featureEta = 'TBD' }
    if ([string]::IsNullOrWhiteSpace($phaseEta)) { $phaseEta = 'TBD' }
    if ([string]::IsNullOrWhiteSpace($roadmapEta)) { $roadmapEta = 'TBD' }

    if ($null -eq $Snapshot.active_feature) {
        if ($Compact) {
            return ('Summary: {0} | {1}' -f $featureLabel, $velocityText)
        }

        return ('Summary: {0} | Velocity {1}' -f $featureLabel, $velocityText)
    }

    if ($Compact) {
        $statusSegment = if ($null -ne $phaseText) { "$statusText · $phaseText" } else { $statusText }
        return ('Summary: {0} | {1} | {2}' -f $featureLabel, $statusSegment, $velocityText)
    }

    $phaseSegment = if ($null -ne $phaseText) { " · phase $phaseText" } else { '' }
    return ('Summary: {0} ({1}{2}) | Velocity {3}' -f $featureLabel, $statusText, $phaseSegment, $velocityText)
}

function ConvertTo-SpecrewDashboardLines {
    param([Parameter(Mandatory = $true)][object]$Snapshot)

    $palette = Get-SpecrewDashboardGlyphPalette -RenderProfile $Snapshot.render_profile
    $lines = New-Object System.Collections.Generic.List[string]
    $rule = $palette.RuleChar * 72
    $lines.Add('SPECREW VELOCITY DASHBOARD') | Out-Null
    $lines.Add($rule) | Out-Null
    $lines.Add(('Today: {0} | Captured: {1}' -f $Snapshot.today_anchor, $Snapshot.captured_at)) | Out-Null
    $lines.Add(('Repo: {0} | Branch: {1}' -f $Snapshot.repository_identity.name, $Snapshot.repository_identity.branch)) | Out-Null
    $lines.Add(('Rendering: {0}' -f $(if ($Snapshot.render_profile.rendering_mode -eq 'rich') { 'rich default' } else { 'monochrome-safe fallback' }))) | Out-Null
    $lines.Add($Snapshot.summary_line) | Out-Null
    $lines.Add('') | Out-Null

    $lines.Add('ACTIVE WORK') | Out-Null
    if ($null -ne $Snapshot.active_feature) {
        $lines.Add(('Feature: {0} {1} | {2} | status {3}' -f $palette.ActiveArrow, (Get-SpecrewFeatureDisplayCode -FeatureRef $Snapshot.active_feature.feature_ref), (Get-SpecrewFeatureReadableTitle -FeatureTitle $Snapshot.active_feature.feature_title -FeatureRef $Snapshot.active_feature.feature_ref), $Snapshot.active_feature.feature_status)) | Out-Null
        if ($null -ne $Snapshot.active_iteration) {
            $phaseHighlight = if (-not [string]::IsNullOrWhiteSpace([string]$Snapshot.active_iteration.state_phase)) { $Snapshot.active_iteration.state_phase.ToUpperInvariant() } else { 'UNKNOWN' }
            $startedText = if ($null -ne $Snapshot.active_iteration.started_at) { $Snapshot.active_iteration.started_at.ToString('yyyy-MM-dd') } else { 'unknown-start' }
            $lines.Add(('Iteration: {0} | phase {1} | started {2}' -f $Snapshot.active_iteration.iteration_label, $phaseHighlight, $startedText)) | Out-Null
            $lines.Add(('In-flight: {0:0.#} SP planned | {1:0.#} SP delivered | {2:0.#} SP remaining' -f $Snapshot.active_feature.planned_story_points, $Snapshot.active_feature.delivered_story_points, $Snapshot.active_feature.remaining_story_points)) | Out-Null
        }
        else {
            $lines.Add('No active iteration is recorded for the current feature.') | Out-Null
        }
    }
    else {
        $lines.Add('No active feature is set. Start or resume a feature to populate this section.') | Out-Null
    }
    $lines.Add('') | Out-Null

    $lines.Add('VELOCITY') | Out-Null
    if ($Snapshot.velocity_headline.sample_size -gt 0) {
        $lines.Add(('Headline: {0:0.##} SP/day | confidence {1}' -f $Snapshot.velocity_headline.points_per_day, $Snapshot.velocity_headline.confidence)) | Out-Null
        $lines.Add(('Sample basis: {0}' -f $Snapshot.velocity_headline.sample_basis_text)) | Out-Null
        $sparkline = Get-SpecrewVelocitySparkline -Values $Snapshot.velocity_headline.recent_values -RenderProfile $Snapshot.render_profile
        if (-not [string]::IsNullOrWhiteSpace($sparkline)) {
            $lines.Add(('Sparkline: {0} | values {1}' -f $sparkline, $Snapshot.velocity_headline.trend_tokens)) | Out-Null
        }
        elseif (-not [string]::IsNullOrWhiteSpace([string]$Snapshot.velocity_headline.insufficient_history_message)) {
            $lines.Add(('Trend: {0}' -f $Snapshot.velocity_headline.insufficient_history_message)) | Out-Null
        }
        else {
            $lines.Add(('Trend: {0}' -f $Snapshot.velocity_headline.trend_tokens)) | Out-Null
        }
    }
    else {
        $lines.Add('Headline: waiting for the first closed iteration') | Out-Null
        $lines.Add('Trend: Need at least one closed iteration before velocity can be calculated.') | Out-Null
    }
    $lines.Add('') | Out-Null

    $lines.Add('RECENT SHIPPED') | Out-Null
    if ($Snapshot.recent_shipped.Count -eq 0) {
        $lines.Add('No shipped iterations yet. Close the first iteration to seed shipped history.') | Out-Null
    }
    else {
        $maxDelivered = ($Snapshot.recent_shipped | Measure-Object -Property delivered_story_points -Maximum).Maximum
        foreach ($entry in $Snapshot.recent_shipped) {
            $meter = Format-SpecrewDashboardMeter -Value $entry.delivered_story_points -Maximum $maxDelivered -Width $Snapshot.render_profile.bar_width -RenderProfile $Snapshot.render_profile
            $iterationToken = Get-SpecrewRecentShippedIterationToken -IterationLabel $entry.iteration_label -IterationRef $entry.iteration_ref
            $title = Format-SpecrewDashboardTruncatedText -Text $entry.short_name -MaxWidth 32
            $lines.Add(('{0} {1,-5} {2,-10} {3} {4,5:0.0} SP {5,2} iter {6} {7}' -f $palette.ShippedMarker, $entry.feature_code, $iterationToken, $meter, $entry.delivered_story_points, $entry.iteration_count, $entry.close_date_text, $title)) | Out-Null
        }
    }
    $lines.Add('') | Out-Null

    $lines.Add('RECENT ITERATIONS (PLAN VS REALITY)') | Out-Null
    $lines.Add('Iter Planned Actual Delta Days') | Out-Null
    foreach ($iteration in $Snapshot.recent_variance) {
        $delta = [decimal]$iteration.actual_story_points - [decimal]$iteration.planned_story_points
        $lines.Add(('{0,-20} {1,7:0.#} {2,6:0.#} {3,5:+0.##;-0.##;0} {4,4}' -f $iteration.display_name, $iteration.planned_story_points, $iteration.actual_story_points, $delta, $iteration.elapsed_days)) | Out-Null
    }
    if ($Snapshot.recent_variance.Count -eq 0) {
        $lines.Add('No closed iterations available for variance reporting.') | Out-Null
    }
    $lines.Add('') | Out-Null

    $lines.Add('FULL HISTORY') | Out-Null
    if ($Snapshot.history.Count -eq 0) {
        $lines.Add('No closed iterations available for history.') | Out-Null
    }
    else {
        $historyMax = ($Snapshot.history | Measure-Object -Property actual_story_points -Maximum).Maximum
        foreach ($iteration in $Snapshot.history) {
            $lines.Add((Format-SpecrewDashboardBarRow -Label $iteration.label -Value $iteration.actual_story_points -Maximum $historyMax -Width ([math]::Min(16, $Snapshot.render_profile.bar_width)) -RenderProfile $Snapshot.render_profile)) | Out-Null
        }
    }
    $lines.Add('') | Out-Null

    $lines.Add('ROADMAP') | Out-Null
    if ($Snapshot.roadmap_progress.Count -eq 0) {
        $lines.Add('Roadmap unavailable yet; add .specrew/roadmap.yml (see docs/roadmap-maintenance.md) to enable this section.') | Out-Null
    }
    else {
        $roadmapSpWidth = 12
        $roadmapStatusWidth = 12
        foreach ($phase in $Snapshot.roadmap_progress) {
            $marker = if ($null -ne $Snapshot.active_phase -and $Snapshot.active_phase.phase_id -eq $phase.phase_id) { $palette.ActiveMarker } elseif ($phase.effective_status -eq 'shipped') { $palette.ShippedMarker } else { $palette.QueuedMarker }
            $phaseLabel = if ($null -ne $Snapshot.active_phase -and $Snapshot.active_phase.phase_id -eq $phase.phase_id) { "$($phase.name) (current)" } else { $phase.name }
            $overflowMarker = if ($phase.effective_status -eq 'drifted-over') {
                if ($Snapshot.render_profile.rendering_mode -eq 'rich') { '▶' } else { '>' }
            }
            else {
                ' '
            }
            $bar = Format-SpecrewDashboardProgressBar -Current $phase.derived_shipped_effort_sp -Total $phase.planned_effort_sp -Width 16 -OverflowMarker $overflowMarker -RenderProfile $Snapshot.render_profile
            $spPair = '{0:0.#}/{1:0.#} SP' -f $phase.derived_shipped_effort_sp, $phase.planned_effort_sp
            $descriptionPrefix = ' ' + (' ' * $bar.Length) + ' ' + (' ' * $roadmapSpWidth) + ' ' + (' ' * $roadmapStatusWidth) + ' '
            $lines.Add(('{0} {1} {2,-12} {3,-12} {4}' -f $marker, $bar, $spPair, $phase.effective_status, $phaseLabel)) | Out-Null
            $lines.Add(($descriptionPrefix + (Format-SpecrewRoadmapDescription -Description $phase.description))) | Out-Null
        }
    }
    $lines.Add('') | Out-Null

    $lines.Add('PROJECTION') | Out-Null
    foreach ($scope in $Snapshot.projection.eta_scopes) {
        $remainingText = if ($null -ne $scope.remaining_story_points) { '{0:0.#} SP' -f $scope.remaining_story_points } else { 'n/a' }
        $lines.Add(('{0} remaining: {1} | ETA: {2} | confidence {3}' -f $scope.label, $remainingText, $scope.eta_text, $scope.confidence)) | Out-Null
    }
    $lines.Add('') | Out-Null

    $lines.Add('WARNINGS') | Out-Null
    if ($Snapshot.warnings.Count -eq 0) {
        $lines.Add('No active dashboard warnings.') | Out-Null
    }
    else {
        foreach ($warning in $Snapshot.warnings) {
            $lines.Add(('WARN: {0}' -f $warning)) | Out-Null
        }
    }

    $lines.Add('') | Out-Null
    $lines.Add('FOOTER') | Out-Null
    $lines.Add(('{0} {1}' -f $palette.FooterMarker, $Snapshot.footer_note)) | Out-Null

    return $lines.ToArray()
}

function ConvertTo-SpecrewCompactDashboardLines {
    param([Parameter(Mandatory = $true)][object]$Snapshot)

    $palette = Get-SpecrewDashboardGlyphPalette -RenderProfile $Snapshot.render_profile
    $lines = New-Object System.Collections.Generic.List[string]
    $lines.Add('SPECREW VELOCITY DASHBOARD') | Out-Null
    $lines.Add((Get-SpecrewSummaryLine -Snapshot $Snapshot -Compact)) | Out-Null
    $lines.Add(('Today {0} | Repo {1}' -f $Snapshot.today_anchor, $Snapshot.repository_identity.name)) | Out-Null
    $lines.Add(('Rendering {0}' -f $Snapshot.render_profile.rendering_mode)) | Out-Null
    $lines.Add('ACTIVE') | Out-Null
    $lines.Add($(if ($null -ne $Snapshot.active_feature) {
                '{0} {1} | {2}' -f $palette.ActiveArrow, (Get-SpecrewFeatureDisplayCode -FeatureRef $Snapshot.active_feature.feature_ref), (Get-SpecrewFeatureReadableTitle -FeatureTitle $Snapshot.active_feature.feature_title -FeatureRef $Snapshot.active_feature.feature_ref)
            }
            else { 'No active feature' })) | Out-Null
    $lines.Add('VELOCITY') | Out-Null
    $lines.Add($(if ($Snapshot.velocity_headline.sample_size -gt 0) { '{0:0.##} SP/day | {1}' -f $Snapshot.velocity_headline.points_per_day, $Snapshot.velocity_headline.confidence } else { 'Awaiting first closeout' })) | Out-Null
    $sparkline = Get-SpecrewVelocitySparkline -Values $Snapshot.velocity_headline.recent_values -RenderProfile $Snapshot.render_profile
    $lines.Add($(if (-not [string]::IsNullOrWhiteSpace($sparkline)) { $sparkline } elseif ($Snapshot.velocity_headline.sample_size -gt 0) { $Snapshot.velocity_headline.trend_tokens } else { 'No trend yet' })) | Out-Null
    $lines.Add('RECENT SHIPPED') | Out-Null
    foreach ($entry in @($Snapshot.recent_shipped | Select-Object -First 3)) {
        $lines.Add(('{0} {1} | {2:0.#} SP' -f $palette.ShippedMarker, $entry.label, $entry.delivered_story_points)) | Out-Null
    }
    while ($lines.Count -lt 13) {
        $lines.Add('-') | Out-Null
    }
    $lines.Add('ROADMAP') | Out-Null
    foreach ($phase in @($Snapshot.roadmap_progress | Select-Object -First 2)) {
        $marker = if ($null -ne $Snapshot.active_phase -and $Snapshot.active_phase.phase_id -eq $phase.phase_id) { $palette.ActiveMarker } elseif ($phase.effective_status -eq 'shipped') { $palette.ShippedMarker } else { $palette.QueuedMarker }
        $lines.Add(('{0} {1} {2:0.#}/{3:0.#} SP' -f $marker, $phase.phase_id, $phase.derived_shipped_effort_sp, $phase.planned_effort_sp)) | Out-Null
    }
    while ($lines.Count -lt 17) {
        $lines.Add('-') | Out-Null
    }
    $lines.Add('PROJECTION') | Out-Null
    $etaScopes = @($Snapshot.projection.eta_scopes)
    $featureScope = @($etaScopes | Where-Object { $_.scope_id -eq 'active-feature' } | Select-Object -First 1)
    $phaseScope = @($etaScopes | Where-Object { $_.scope_id -eq 'current-phase' } | Select-Object -First 1)
    $roadmapScope = @($etaScopes | Where-Object { $_.scope_id -eq 'roadmap' } | Select-Object -First 1)
    $featureScope = if ($featureScope.Count -gt 0) { $featureScope[0] } else { $null }
    $phaseScope = if ($phaseScope.Count -gt 0) { $phaseScope[0] } else { $null }
    $roadmapScope = if ($roadmapScope.Count -gt 0) { $roadmapScope[0] } else { $null }
    $lines.Add(('F:{0} {1} | P:{2} {3}' -f ($(if ($null -ne $featureScope -and $null -ne $featureScope.remaining_story_points) { '{0:0.#}SP' -f $featureScope.remaining_story_points } else { 'n/a' })), $(if ($null -ne $featureScope) { $featureScope.eta_text } else { 'TBD' }), ($(if ($null -ne $phaseScope -and $null -ne $phaseScope.remaining_story_points) { '{0:0.#}SP' -f $phaseScope.remaining_story_points } else { 'n/a' })), $(if ($null -ne $phaseScope) { $phaseScope.eta_text } else { 'TBD' }))) | Out-Null
    $lines.Add(('R:{0} {1}' -f ($(if ($null -ne $roadmapScope -and $null -ne $roadmapScope.remaining_story_points) { '{0:0.#}SP' -f $roadmapScope.remaining_story_points } else { 'n/a' })), $(if ($null -ne $roadmapScope) { $roadmapScope.eta_text } else { 'TBD' }))) | Out-Null
    $lines.Add('WARNINGS') | Out-Null
    $warningSummary = if ($Snapshot.warnings.Count -gt 0) { $Snapshot.warnings[0] } else { 'No active dashboard warnings.' }
    if ($warningSummary.Length -gt 84) {
        $warningSummary = $warningSummary.Substring(0, 81) + '...'
    }
    $lines.Add($warningSummary) | Out-Null
    $lines.Add('FOOTER') | Out-Null
    $footer = $Snapshot.footer_note
    if ($footer.Length -gt 84) {
        $footer = $footer.Substring(0, 81) + '...'
    }
    $lines.Add($footer) | Out-Null
    if ($lines.Count -gt 24) {
        return @($lines | Select-Object -First 24)
    }

    while ($lines.Count -lt 24) {
        $lines.Add('') | Out-Null
    }

    return $lines.ToArray()
}

function Write-SpecrewDashboardLines {
    param(
        [AllowEmptyCollection()]
        [AllowEmptyString()]
        [Parameter(Mandatory = $true)][string[]]$Lines,
        [Parameter(Mandatory = $true)][string]$ColorMode
    )

    foreach ($line in $Lines) {
        if ($ColorMode -eq 'semantic-color') {
            if ($line -eq 'SPECREW VELOCITY DASHBOARD' -or $line -like 'Summary:*') {
                Write-Host $line -ForegroundColor Green
                continue
            }

            if ($line -match '^(ACTIVE WORK|VELOCITY|RECENT SHIPPED|RECENT ITERATIONS \(PLAN VS REALITY\)|FULL HISTORY|ROADMAP|PROJECTION|WARNINGS|FOOTER|ACTIVE)$') {
                Write-Host $line -ForegroundColor Cyan
                continue
            }

            if ($line -like 'Today:*' -or $line -like 'Repo:*' -or $line -like 'Rendering:*') {
                Write-Host $line -ForegroundColor DarkGray
                continue
            }

            if ($line -match '^WARN:') {
                Write-Host $line -ForegroundColor Yellow
                continue
            }
        }

        Write-Host $line
    }
}

function Remove-SpecrewAnsiEscapeSequences {
    param([AllowNull()][string]$Text)

    if ($null -eq $Text) {
        return ''
    }

    return [regex]::Replace($Text, ([string][char]27 + '\[[0-9;]*[A-Za-z]'), '')
}

function ConvertTo-SpecrewDashboardArtifactContent {
    param(
        [Parameter(Mandatory = $true)][object]$Snapshot,
        [AllowEmptyCollection()]
        [AllowEmptyString()]
        [Parameter(Mandatory = $true)][string[]]$Lines,
        [Parameter(Mandatory = $true)][ValidateSet('live', 'iteration-closeout', 'feature-closeout')][string]$CaptureKind,
        [AllowNull()][string]$HistoricalNotice
    )

    $notice = if ([string]::IsNullOrWhiteSpace($HistoricalNotice)) {
        if ($CaptureKind -eq 'iteration-closeout') {
            'Historical snapshot captured during iteration closeout. Re-running the dashboard later produces a new live view and must not overwrite this file.'
        }
        elseif ($CaptureKind -eq 'feature-closeout') {
            'Historical snapshot captured during feature closeout. Re-running the dashboard later produces a new live view and must not overwrite this file.'
        }
        else {
            'Live dashboard snapshot.'
        }
    }
    else {
        $HistoricalNotice
    }

    $lineFeed = "`n"
    $dashboardText = Remove-SpecrewAnsiEscapeSequences -Text ($Lines -join $lineFeed)

    return @(
        '# Velocity Dashboard Snapshot',
        '',
        '**Schema**: v1',
        ('**Capture Kind**: {0}' -f $CaptureKind),
        ('**Captured At**: {0}' -f $Snapshot.captured_at),
        ('**Render Mode**: {0}' -f $Snapshot.render_mode),
        ('**Rendering Mode**: {0}' -f $Snapshot.render_profile.rendering_mode),
        ('**Color Mode**: {0}' -f $Snapshot.color_mode),
        ('**Historical Notice**: {0}' -f $notice),
        '',
        '## Dashboard',
        '',
        '```text',
        $dashboardText,
        '```',
        ''
    ) -join $lineFeed
}