Payload/scripts/kaden/advisory/AdvisoryTurnCorrelation.ps1

# Dot-sourced into AdvisoryWorkflow.psm1 - journal-derived turn correlation.

function Get-AdvisoryAbsentTurnCorrelation {
    return [pscustomobject]@{
        turn_correlation = 'absent'
        turn_context     = [pscustomobject]@{ turn_state = 'absent'; turn_id = $null; slice = $null; is_active = $false }
    }
}

function Get-AdvisoryWorkflowJournalTurnRowSet {
    param([Parameter(Mandatory = $true)][string]$FeatureRoot)
    $events = @()
    if (Get-Command Get-HarnessJournalEvents -ErrorAction SilentlyContinue) {
        $events = @(Get-HarnessJournalEvents -FeatureRoot $FeatureRoot)
    } elseif (Test-Path -LiteralPath (Join-Path $FeatureRoot 'journal\events.jsonl')) {
        foreach ($line in @(Get-Content -LiteralPath (Join-Path $FeatureRoot 'journal\events.jsonl'))) {
            if ([string]::IsNullOrWhiteSpace($line)) { continue }
            $events += ($line | ConvertFrom-Json)
        }
    }
    $turnStarts = @()
    $turnStops = @()
    foreach ($evt in $events) {
        $kind = [string](Get-AdvisoryWorkflowProperty -Record $evt -Name 'kind')
        $activity = Get-AdvisoryWorkflowProperty -Record $evt -Name 'activity'
        $activityType = [string](Get-AdvisoryWorkflowProperty -Record $activity -Name 'type')
        $activityName = [string](Get-AdvisoryWorkflowProperty -Record $activity -Name 'name')
        $refs = Get-AdvisoryWorkflowProperty -Record $evt -Name 'refs'
        $turnId = [string](Get-AdvisoryWorkflowProperty -Record $refs -Name 'turn_id')
        if ([string]::IsNullOrWhiteSpace($turnId)) {
            $turnId = [string](Get-AdvisoryWorkflowProperty -Record $activity -Name 'id')
        }
        $slice = [string](Get-AdvisoryWorkflowProperty -Record $evt -Name 'slice')
        $isTurn = ($activityType -eq 'turn') -or ($activityName -eq 'start') -or ($activityName -eq 'stop')
        if (-not $isTurn) { continue }
        $row = [pscustomobject]@{
            seq     = [int](Get-AdvisoryWorkflowProperty -Record $evt -Name 'seq')
            kind    = $kind
            turn_id = $turnId
            slice   = $slice
            name    = $activityName
            summary = [string](Get-AdvisoryWorkflowProperty -Record $evt -Name 'summary')
        }
        if ($kind -eq 'ActivityStarted' -and ($activityName -eq 'start' -or $activityType -eq 'turn')) {
            $turnStarts += $row
        }
        if ($kind -eq 'ActivityStopped' -and ($activityName -eq 'stop' -or $activityType -eq 'turn')) {
            $turnStops += $row
        }
    }
    return [pscustomobject]@{ starts = @($turnStarts); stops = @($turnStops) }
}

function Resolve-AdvisoryStoppedOrStaleTurn {
    param($LatestStop)
    $summary = [string](Get-AdvisoryWorkflowProperty -Record $LatestStop -Name 'summary')
    $state = if ($summary -match '(?i)stale|abandon') { 'stale' } else { 'stopped' }
    return [pscustomobject]@{
        turn_correlation = $state
        turn_context     = [pscustomobject]@{
            turn_state = $state
            turn_id    = [string]$LatestStop.turn_id
            slice      = [string]$LatestStop.slice
            is_active  = $false
        }
    }
}

function Resolve-AdvisoryWorkflowTurnCorrelation {
    param([Parameter(Mandatory = $true)][string]$FeatureRoot)
    $rows = Get-AdvisoryWorkflowJournalTurnRowSet -FeatureRoot $FeatureRoot
    if ($rows.starts.Count -eq 0 -and $rows.stops.Count -eq 0) {
        return Get-AdvisoryAbsentTurnCorrelation
    }

    $latestStart = $null
    $latestStop = $null
    $sortedStarts = @($rows.starts | Sort-Object seq)
    $sortedStops = @($rows.stops | Sort-Object seq)
    if ($sortedStarts.Count -gt 0) { $latestStart = $sortedStarts[-1] }
    if ($sortedStops.Count -gt 0) { $latestStop = $sortedStops[-1] }
    $open = $false
    if ($null -ne $latestStart) {
        $open = ($null -eq $latestStop) -or ([int]$latestStop.seq -lt [int]$latestStart.seq)
    }

    if ($open) {
        return [pscustomobject]@{
            turn_correlation = 'open'
            turn_context     = [pscustomobject]@{
                turn_state = 'open'
                turn_id    = [string]$latestStart.turn_id
                slice      = [string]$latestStart.slice
                is_active  = $true
            }
        }
    }

    if ($null -ne $latestStop) {
        return Resolve-AdvisoryStoppedOrStaleTurn -LatestStop $latestStop
    }

    return Get-AdvisoryAbsentTurnCorrelation
}

function Test-AdvisoryWorkflowTurnContextAgreement {
    param(
        $Derived,
        $Caller
    )
    if ($null -eq $Caller) { return $true }
    $callerTurnId = [string](Get-AdvisoryWorkflowProperty -Record $Caller -Name 'turn_id')
    $callerState = [string](Get-AdvisoryWorkflowProperty -Record $Caller -Name 'turn_state')
    $callerSlice = [string](Get-AdvisoryWorkflowProperty -Record $Caller -Name 'slice')
    $derivedContext = Get-AdvisoryWorkflowProperty -Record $Derived -Name 'turn_context'
    $derivedTurnId = [string](Get-AdvisoryWorkflowProperty -Record $derivedContext -Name 'turn_id')
    $derivedState = [string](Get-AdvisoryWorkflowProperty -Record $Derived -Name 'turn_correlation')
    $derivedSlice = [string](Get-AdvisoryWorkflowProperty -Record $derivedContext -Name 'slice')
    if ((-not [string]::IsNullOrWhiteSpace($callerTurnId)) -and ($callerTurnId -ne $derivedTurnId)) { return $false }
    if ((-not [string]::IsNullOrWhiteSpace($callerState)) -and ($callerState -ne $derivedState) -and ($callerState -ne [string](Get-AdvisoryWorkflowProperty -Record $derivedContext -Name 'turn_state'))) { return $false }
    if ((-not [string]::IsNullOrWhiteSpace($callerSlice)) -and ($callerSlice -ne $derivedSlice)) { return $false }
    return $true
}

function Resolve-AdvisoryWorkflowTurnGate {
    <#
    .SYNOPSIS
        Derive turn correlation and reject a disagreeing caller envelope.
    #>

    param(
        [Parameter(Mandatory = $true)]$Request,
        [Parameter(Mandatory = $true)][string]$ActionName,
        [Parameter(Mandatory = $true)][string]$ActionId
    )
    $featureRoot = [string]$Request.FeatureRoot
    $derived = Resolve-AdvisoryWorkflowTurnCorrelation -FeatureRoot $featureRoot
    $caller = ConvertFrom-AdvisoryWorkflowJson -Text ([string](Get-AdvisoryWorkflowProperty -Record $Request -Name 'TurnContextJson'))
    if (-not (Test-AdvisoryWorkflowTurnContextAgreement -Derived $derived -Caller $caller)) {
        return [pscustomobject]@{
            rejected         = $true
            result           = (New-AdvisoryWorkflowFailure -Status 'rejected' -Code 'TURN_CONTEXT_DISAGREEMENT' -Action $ActionName -ActionId $ActionId `
                    -Message 'Caller turn context disagrees with the journal-derived turn correlation.')
            turn_context     = $null
            turn_correlation = $null
            derived          = $derived
        }
    }
    return [pscustomobject]@{
        rejected         = $false
        result           = $null
        turn_context     = (Get-AdvisoryWorkflowProperty -Record $derived -Name 'turn_context')
        turn_correlation = [string](Get-AdvisoryWorkflowProperty -Record $derived -Name 'turn_correlation')
        derived          = $derived
    }
}