Payload/scripts/kaden/advisory/AdvisoryDecisionSlate.ps1

# Dot-sourced into AdvisoryWorkflow.psm1 - numbered decision slate binding.

function Get-AdvisorySlateTrimmedGroup {
    param($Group)
    if ($null -eq $Group) { return '' }
    return ([string]$Group).Trim()
}

function New-AdvisorySlateLineToken {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Returns an in-memory slate token. It does not change system state.')]
    param(
        [Parameter(Mandatory = $true)][int]$Position,
        [Parameter(Mandatory = $true)][string]$Decision,
        [string]$Direction = '',
        [string]$Owner = '',
        [switch]$OwnerNameOmitted
    )
    return [pscustomobject]@{
        ok                 = $true
        position           = $Position
        decision           = $Decision
        direction          = $Direction
        owner              = $Owner
        owner_name_omitted = [bool]$OwnerNameOmitted
    }
}

function ConvertFrom-AdvisorySlateLineToken {
    <#
    .SYNOPSIS
        Parses one numbered slate line into a decision, an optional named owner, and an optional rationale.
    .DESCRIPTION
        `3 Fix owner spike-runner` is a Fix whose next owner is the named token. The finding's route stays the destination.
        A numbered Fix that names no owner leaves owner empty. The binder then uses the route default, or returns the finding id in needs_owner when the route has none.
        `3 Fix owner` with the name omitted is not a default-owner Fix. That finding id is returned in needs_owner.
    #>

    param([Parameter(Mandatory = $true)][string]$Line)
    $labels = @(
        @{ Pattern = '^\s*(\d+)\s+Accept\s+Risk(?:\s*(?:\u2014|-)\s*(.*))?$'; Decision = 'Accept Risk' }
        @{ Pattern = '^\s*(\d+)\s+Promote\s+To\s+Backlog(?:\s*(?:\u2014|-)\s*(.*))?$'; Decision = 'Promote To Backlog' }
        @{ Pattern = '^\s*(\d+)\s+More\s+Info(?:\s*(?:\u2014|-)\s*(.*))?$'; Decision = 'More Info' }
    )
    foreach ($label in $labels) {
        if ($Line -match [string]$label.Pattern) {
            return New-AdvisorySlateLineToken -Position ([int]$Matches[1]) -Decision ([string]$label.Decision) -Direction (Get-AdvisorySlateTrimmedGroup $Matches[2])
        }
    }
    if ($Line -match '^\s*(\d+)\s+Fix\s+owner\s*$') {
        return New-AdvisorySlateLineToken -Position ([int]$Matches[1]) -Decision 'Fix' -OwnerNameOmitted
    }
    if ($Line -match '^\s*(\d+)\s+Fix\s+owner\s+(\S+)(?:\s*(?:\u2014|-)\s*(.*))?$') {
        return New-AdvisorySlateLineToken -Position ([int]$Matches[1]) -Decision 'Fix' -Owner ([string]$Matches[2]) -Direction (Get-AdvisorySlateTrimmedGroup $Matches[3])
    }
    if ($Line -match '^\s*(\d+)\s+(\S+)(?:\s*(?:\u2014|-)\s*(.*))?$') {
        return New-AdvisorySlateLineToken -Position ([int]$Matches[1]) -Decision ([string]$Matches[2]) -Direction (Get-AdvisorySlateTrimmedGroup $Matches[3])
    }
    return [pscustomobject]@{ ok = $false; position = 0; decision = ''; direction = ''; owner = ''; owner_name_omitted = $false }
}

function New-AdvisorySlateBoundRow {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Returns an in-memory bound slate row. It does not change system state.')]
    param(
        [Parameter(Mandatory = $true)][int]$Position,
        [Parameter(Mandatory = $true)][string]$FindingId,
        [Parameter(Mandatory = $true)][string]$Decision,
        [string]$Rationale = '',
        [string]$Destination = '',
        $Owner = $null,
        [string]$Trigger = '',
        [string]$Status = 'Open'
    )
    return [pscustomobject]@{
        position                      = $Position
        finding_id                    = $FindingId
        decision                      = $Decision
        rationale                     = $Rationale
        selected_work_destination     = $Destination
        next_owner                    = $Owner
        trigger_or_needed_information = $Trigger
        status                        = $Status
    }
}

function New-AdvisorySlateBindResult {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Returns an in-memory slate bind result. It does not change system state.')]
    param(
        $Bound,
        $NeedsDirection,
        $NeedsOwner
    )
    return [pscustomobject]@{
        direction_satisfied = (@($NeedsDirection).Count -eq 0)
        rows            = @($Bound)
        needs_direction = @($NeedsDirection)
        needs_owner     = @($NeedsOwner)
    }
}

function Get-AdvisorySlateDecisionPolicy {
    param([Parameter(Mandatory = $true)][string]$Decision)
    switch ($Decision) {
        'Defer' { return @{ RequiresDirection = $true; CopyDirectionToTrigger = $true; Destination = 'deferred-work'; AcceptRisk = $false } }
        'Promote To Backlog' { return @{ RequiresDirection = $true; CopyDirectionToTrigger = $true; Destination = $null; AcceptRisk = $false } }
        'More Info' { return @{ RequiresDirection = $true; CopyDirectionToTrigger = $true; Destination = $null; AcceptRisk = $false } }
        'Accept Risk' { return @{ RequiresDirection = $false; CopyDirectionToTrigger = $false; Destination = 'none'; AcceptRisk = $true } }
        default { return @{ RequiresDirection = $false; CopyDirectionToTrigger = $false; Destination = $null; AcceptRisk = $false } }
    }
}

function Add-AdvisorySlateAllFixRow {
    param(
        $Findings,
        $Bound,
        $NeedsOwner
    )
    for ($i = 0; $i -lt @($Findings).Count; $i++) {
        $finding = @($Findings)[$i]
        $route = [string](Get-AdvisoryWorkflowProperty -Record $finding -Name 'route_to')
        $owner = Get-AdvisoryDefaultOwnerForRoute -RouteTo $route
        $findingId = [string](Get-AdvisoryWorkflowProperty -Record $finding -Name 'finding_id')
        if ([string]::IsNullOrWhiteSpace([string]$owner)) {
            [void]$NeedsOwner.Add($findingId)
            continue
        }
        $recommendation = [string](Get-AdvisoryWorkflowProperty -Record $finding -Name 'recommendation')
        [void]$Bound.Add((New-AdvisorySlateBoundRow -Position ($i + 1) -FindingId $findingId -Decision 'Fix' -Rationale $recommendation -Destination $route -Owner $owner))
    }
}

function Add-AdvisorySlateNumberedRow {
    param(
        [string[]]$Lines,
        $Findings,
        $Bound,
        $NeedsDirection,
        $NeedsOwner
    )
    foreach ($line in @($Lines)) {
        $token = ConvertFrom-AdvisorySlateLineToken -Line $line
        if (-not $token.ok) { continue }
        $position = [int]$token.position
        $decision = [string]$token.decision
        $direction = [string]$token.direction
        if ($position -lt 1 -or $position -gt @($Findings).Count) { continue }
        $finding = @($Findings)[$position - 1]
        $findingId = [string](Get-AdvisoryWorkflowProperty -Record $finding -Name 'finding_id')
        $route = [string](Get-AdvisoryWorkflowProperty -Record $finding -Name 'route_to')
        $recommendation = [string](Get-AdvisoryWorkflowProperty -Record $finding -Name 'recommendation')
        $policy = Get-AdvisorySlateDecisionPolicy -Decision $decision
        if ($policy.RequiresDirection -and [string]::IsNullOrWhiteSpace($direction)) {
            [void]$NeedsDirection.Add([pscustomobject]@{ position = $position; finding_id = $findingId; decision = $decision })
            continue
        }
        $owner = Get-AdvisoryDefaultOwnerForRoute -RouteTo $route
        $namedOwner = [string]$token.owner
        if ($decision -eq 'Fix' -and $token.owner_name_omitted) {
            [void]$NeedsOwner.Add($findingId)
            continue
        }
        if ($decision -eq 'Fix' -and -not [string]::IsNullOrWhiteSpace($namedOwner)) {
            $owner = $namedOwner
        } elseif ($decision -eq 'Fix' -and [string]::IsNullOrWhiteSpace([string]$owner)) {
            [void]$NeedsOwner.Add($findingId)
            continue
        }
        $destination = $route
        $status = 'Open'
        $rationale = if ([string]::IsNullOrWhiteSpace($direction)) { $recommendation } else { $direction }
        $trigger = ''
        if ($policy.CopyDirectionToTrigger) { $trigger = $direction }
        if ($null -ne $policy.Destination) { $destination = [string]$policy.Destination }
        if ($policy.AcceptRisk) {
            $owner = $null
            $status = 'Accepted'
        }
        [void]$Bound.Add((New-AdvisorySlateBoundRow -Position $position -FindingId $findingId -Decision $decision -Rationale $rationale -Destination $destination -Owner $owner -Trigger $trigger -Status $status))
    }
}

function ConvertFrom-AdvisoryDecisionSlate {
    <#
    .SYNOPSIS
        Binds a decision slate to the open findings on an advisory record.
    .DESCRIPTION
        `3 Fix owner spike-runner` records Fix with that next owner. The selected work destination is the finding's route.
        A numbered Fix that names no owner records the route's default owner when the route has one.
        A numbered Fix on a route with no default owner is not bound, and its finding id is returned in needs_owner.
        `3 Fix owner` with the name omitted is the same refusal, even when the route has a default owner.
        A named owner on specification must be spec-writer, and a named owner on domain-design must be ddd-architect. Investigation and deferred-work keep the named owner.
        `all Fix` records Fix only for open findings whose route has a default owner. Skipped finding ids are listed in needs_owner and stay open.
    #>

    param(
        [Parameter(Mandatory = $true)][string]$SlateReply,
        [Parameter(Mandatory = $true)]$Assessment
    )
    $findings = @((Get-AdvisoryWorkflowProperty -Record $Assessment -Name 'findings'))
    $lines = @($SlateReply -split "`r?`n" | ForEach-Object { $_.Trim() } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
    $bound = [System.Collections.Generic.List[object]]::new()
    $needsDirection = [System.Collections.Generic.List[object]]::new()
    $needsOwner = [System.Collections.Generic.List[string]]::new()
    $expandAllFix = $false
    foreach ($line in $lines) {
        if ($line -match '(?i)^all\s+Fix$') { $expandAllFix = $true; break }
    }
    if ($expandAllFix) {
        Add-AdvisorySlateAllFixRow -Findings $findings -Bound $bound -NeedsOwner $needsOwner
    } else {
        Add-AdvisorySlateNumberedRow -Lines $lines -Findings $findings -Bound $bound -NeedsDirection $needsDirection -NeedsOwner $needsOwner
    }
    return New-AdvisorySlateBindResult -Bound $bound -NeedsDirection $needsDirection -NeedsOwner $needsOwner
}