Payload/scripts/kaden/advisory/AdvisoryWorkflow.psm1

#Requires -Version 5.1
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

<#
.SYNOPSIS
    Public advisory continuity workflow behind harness-advisory.ps1.
.DESCRIPTION
    Issues, decides, amends, and routes advisory reports; rebuilds the
    journal-derived advisory index; resumes from that index; and reports
    explicit gate-projection exclusion. Callers enter through
    harness-advisory.ps1. This module does not treat advisory state as a gate.
#>


$script:AdvisoryWorkflowActionIdPattern = '^advact-[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'
$script:AdvisoryWorkflowReportNamePattern = '^second-opinion-(spec|feature|milestone)-iter([1-9][0-9]*)\.md$'
$script:AdvisoryWorkflowExcludedEventKind = 'AdvisoryContinuityRecorded'

$contractPath = Join-Path $PSScriptRoot 'AdvisoryReportContract.psm1'
$harnessLib = Join-Path (Split-Path $PSScriptRoot -Parent) 'harness\lib'
Import-Module $contractPath -Force
Import-Module (Join-Path $harnessLib 'HarnessContract.psm1') -Force
# Journal and projection commands are called from the contract module, which
# only sees the global session. Import them globally so that call succeeds.
Import-Module (Join-Path $harnessLib 'HarnessJournal.psm1') -Force -Global
Import-Module (Join-Path $harnessLib 'HarnessGateProjection.psm1') -Force -Global

function Get-AdvisoryWorkflowProperty {
    param($Record, [Parameter(Mandatory = $true)][string]$Name)
    if ($null -eq $Record) { return $null }
    $property = $Record.PSObject.Properties[$Name]
    if ($null -eq $property) { return $null }
    return $property.Value
}

function Test-AdvisoryWorkflowBlank {
    param($Value)
    return ($null -eq $Value) -or ([string]::IsNullOrWhiteSpace([string]$Value))
}

function Get-AdvisoryWorkflowSha256 {
    param([AllowNull()][string]$Text = '')
    $sha = [System.Security.Cryptography.SHA256]::Create()
    try {
        $bytes = [System.Text.Encoding]::UTF8.GetBytes([string]$Text)
        $hash = $sha.ComputeHash($bytes)
        return ([System.BitConverter]::ToString($hash) -replace '-', '').ToLowerInvariant()
    } finally {
        $sha.Dispose()
    }
}

function Get-AdvisoryWorkflowFileSha256 {
    param([Parameter(Mandatory = $true)][string]$Path)
    $sha = [System.Security.Cryptography.SHA256]::Create()
    try {
        $bytes = [System.IO.File]::ReadAllBytes($Path)
        $hash = $sha.ComputeHash($bytes)
        return ([System.BitConverter]::ToString($hash) -replace '-', '').ToLowerInvariant()
    } finally {
        $sha.Dispose()
    }
}

function ConvertTo-AdvisoryWorkflowCanonicalJson {
    param($Value)
    if ($null -eq $Value) { return 'null' }
    if ($Value -is [string]) { return (ConvertTo-Json -InputObject $Value -Compress) }
    if ($Value -is [bool]) { if ($Value) { return 'true' } else { return 'false' } }
    if ($Value -is [byte] -or $Value -is [int] -or $Value -is [long] -or $Value -is [decimal] -or $Value -is [double] -or $Value -is [single]) {
        return (ConvertTo-Json -InputObject $Value -Compress)
    }
    $isArray = ($Value -is [System.Array]) -or ($Value -is [System.Collections.IList] -and -not ($Value -is [string]))
    if ($isArray) {
        $items = @()
        foreach ($item in @($Value)) { $items += (ConvertTo-AdvisoryWorkflowCanonicalJson -Value $item) }
        return '[' + ($items -join ',') + ']'
    }
    if ($Value -is [System.Collections.IDictionary] -or $Value -is [pscustomobject]) {
        $names = @($Value.PSObject.Properties.Name | Sort-Object)
        $pairs = @()
        foreach ($name in $names) {
            $pairs += ((ConvertTo-Json -InputObject ([string]$name) -Compress) + ':' + (ConvertTo-AdvisoryWorkflowCanonicalJson -Value $Value.$name))
        }
        return '{' + ($pairs -join ',') + '}'
    }
    return (ConvertTo-Json -InputObject ([string]$Value) -Compress)
}

function ConvertFrom-AdvisoryWorkflowJson {
    param([AllowNull()][string]$Text)
    if (Test-AdvisoryWorkflowBlank $Text) { return $null }
    return (ConvertFrom-HarnessJson -Json $Text)
}

function ConvertFrom-AdvisoryWorkflowJsonArray {
    param([AllowNull()][string]$Text)
    $parsed = ConvertFrom-AdvisoryWorkflowJson -Text $Text
    if ($null -eq $parsed) { return @() }
    return @($parsed)
}

function New-AdvisoryWorkflowDiagnostic {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Returns an in-memory diagnostic. It does not change system state.')]
    param(
        [string]$Path = '',
        [Parameter(Mandatory = $true)][string]$Code,
        [Parameter(Mandatory = $true)][string]$ObservedProblem,
        [string]$Provenance = 'report-scan'
    )
    return [pscustomobject]@{
        path              = $Path
        code              = $Code
        reason            = $ObservedProblem
        observed_problem  = $ObservedProblem
        provenance        = $Provenance
    }
}

function Get-AdvisoryWorkflowAdvisoryDirectory {
    param([Parameter(Mandatory = $true)][string]$FeatureRoot)
    return Join-Path $FeatureRoot 'advisory'
}

function Get-AdvisoryWorkflowIndexPath {
    param([Parameter(Mandatory = $true)][string]$FeatureRoot)
    return Join-Path (Get-AdvisoryWorkflowAdvisoryDirectory -FeatureRoot $FeatureRoot) 'advisory-index.jsonl'
}

function Get-AdvisoryWorkflowReceiptPath {
    param([Parameter(Mandatory = $true)][string]$FeatureRoot)
    return Join-Path (Get-AdvisoryWorkflowAdvisoryDirectory -FeatureRoot $FeatureRoot) 'action-receipts.json'
}

function Get-AdvisoryWorkflowRepoRelativeReportPath {
    param(
        [Parameter(Mandatory = $true)][string]$Feature,
        [Parameter(Mandatory = $true)][string]$FileName
    )
    return "docs/harness/features/$Feature/advisory/$FileName"
}

function Get-AdvisoryWorkflowRepoRelativeIndexPath {
    param([Parameter(Mandatory = $true)][string]$Feature)
    return "docs/harness/features/$Feature/advisory/advisory-index.jsonl"
}

function Get-AdvisoryWorkflowReportFile {
    param([Parameter(Mandatory = $true)][string]$FeatureRoot)
    $dir = Get-AdvisoryWorkflowAdvisoryDirectory -FeatureRoot $FeatureRoot
    if (-not (Test-Path -LiteralPath $dir)) { return @() }
    return @(Get-ChildItem -LiteralPath $dir -File | Where-Object { $_.Name -match $script:AdvisoryWorkflowReportNamePattern } | Sort-Object Name)
}

function Test-AdvisoryWorkflowHistoricalText {
    param([AllowNull()][string]$Text)
    if (Test-AdvisoryWorkflowBlank $Text) { return $false }
    if ($Text -match '(?m)artifact_status\s*[:=]\s*historical-legacy') { return $true }
    if ($Text -match '(?m)discovery_candidate\s*[:=]\s*false') { return $true }
    return $false
}

function Read-AdvisoryWorkflowReportText {
    param([Parameter(Mandatory = $true)][string]$Path)
    $bytes = [System.IO.File]::ReadAllBytes($Path)
    return [pscustomobject]@{
        bytes = $bytes
        text  = [System.Text.Encoding]::UTF8.GetString($bytes)
        hash  = (Get-AdvisoryWorkflowSha256 -Text ([System.Text.Encoding]::UTF8.GetString($bytes)))
    }
}

function Get-AdvisoryWorkflowRecordFromText {
    param([AllowNull()][string]$Text)
    $body = Get-AdvisoryWorkflowRecordFenceBody -Text $Text
    if ($null -eq $body) { return $null }
    try {
        return (ConvertFrom-HarnessJson -Json $body)
    } catch {
        return $null
    }
}

. (Join-Path $PSScriptRoot 'AdvisoryOperatorSurface.ps1')
. (Join-Path $PSScriptRoot 'AdvisoryTurnCorrelation.ps1')
. (Join-Path $PSScriptRoot 'AdvisoryDecisionSlate.ps1')

function Save-AdvisoryWorkflowTextAtomic {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Local advisory file replace. Callers already decided to persist.')]
    param(
        [Parameter(Mandatory = $true)][string]$Path,
        [Parameter(Mandatory = $true)][string]$Content
    )
    $parent = Split-Path -Parent $Path
    if (-not (Test-Path -LiteralPath $parent)) {
        New-Item -ItemType Directory -Path $parent -Force | Out-Null
    }
    $temp = $Path + '.tmp'
    $utf8 = New-Object System.Text.UTF8Encoding $false
    [System.IO.File]::WriteAllText($temp, $Content, $utf8)
    if (Test-Path -LiteralPath $Path) {
        [System.IO.File]::Replace($temp, $Path, [NullString]::Value, $true)
    } else {
        [System.IO.File]::Move($temp, $Path)
    }
}

function ConvertTo-AdvisoryWorkflowReportMarkdown {
    param($Assessment)
    return ConvertTo-AdvisoryReadableReportMarkdown -Assessment $Assessment
}

function Save-AdvisoryWorkflowReport {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Persists one advisory report under the feature advisory directory.')]
    param(
        [Parameter(Mandatory = $true)][string]$FeatureRoot,
        [Parameter(Mandatory = $true)]$Assessment
    )
    $relative = [string](Get-AdvisoryWorkflowProperty -Record $Assessment -Name 'relative_path')
    $fileName = Split-Path -Leaf ($relative -replace '/', '\')
    $path = Join-Path (Get-AdvisoryWorkflowAdvisoryDirectory -FeatureRoot $FeatureRoot) $fileName
    Save-AdvisoryWorkflowTextAtomic -Path $path -Content (ConvertTo-AdvisoryWorkflowReportMarkdown -Assessment $Assessment)
    return $path
}

function Get-AdvisoryWorkflowJournalEvent {
    param([Parameter(Mandatory = $true)][string]$FeatureRoot)
    if (-not (Test-Path -LiteralPath (Join-Path $FeatureRoot 'journal\events.jsonl'))) { return @() }
    return @(Get-HarnessJournalEvents -FeatureRoot $FeatureRoot)
}

function Get-AdvisoryWorkflowContinuityEvent {
    param([Parameter(Mandatory = $true)][string]$FeatureRoot)
    return @(Get-AdvisoryWorkflowJournalEvent -FeatureRoot $FeatureRoot | Where-Object {
            [string](Get-AdvisoryWorkflowProperty -Record $_ -Name 'kind') -eq $script:AdvisoryWorkflowExcludedEventKind
        })
}

function Find-AdvisoryWorkflowStoredAction {
    param(
        [Parameter(Mandatory = $true)][string]$FeatureRoot,
        [Parameter(Mandatory = $true)][string]$ActionId
    )
    foreach ($evt in @(Get-AdvisoryWorkflowContinuityEvent -FeatureRoot $FeatureRoot)) {
        $payload = Get-AdvisoryWorkflowProperty -Record $evt -Name 'payload'
        $storedId = [string](Get-AdvisoryWorkflowProperty -Record $payload -Name 'action_id')
        if ($storedId -ceq $ActionId) {
            return [pscustomobject]@{ source = 'journal'; event = $evt; payload = $payload; receipt = $null }
        }
    }
    $receiptPath = Get-AdvisoryWorkflowReceiptPath -FeatureRoot $FeatureRoot
    if (Test-Path -LiteralPath $receiptPath) {
        $doc = Get-Content -LiteralPath $receiptPath -Raw -Encoding UTF8 | ConvertFrom-Json
        foreach ($receipt in @($doc.receipts)) {
            if ([string](Get-AdvisoryWorkflowProperty -Record $receipt -Name 'action_id') -ceq $ActionId) {
                return [pscustomobject]@{ source = 'receipt'; event = $null; payload = $null; receipt = $receipt }
            }
        }
    }
    return $null
}

function Save-AdvisoryWorkflowReceipt {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Records rebuild action identity outside the journal so index fingerprints stay stable.')]
    param(
        [Parameter(Mandatory = $true)][string]$FeatureRoot,
        [Parameter(Mandatory = $true)]$Receipt
    )
    $path = Get-AdvisoryWorkflowReceiptPath -FeatureRoot $FeatureRoot
    $list = @()
    if (Test-Path -LiteralPath $path) {
        $doc = Get-Content -LiteralPath $path -Raw -Encoding UTF8 | ConvertFrom-Json
        $list = @($doc.receipts)
    }
    $list += $Receipt
    $payload = [pscustomobject]@{ receipts = @($list) }
    Save-AdvisoryWorkflowTextAtomic -Path $path -Content ((ConvertTo-AdvisoryWorkflowCanonicalJson -Value $payload) + "`n")
}

function Add-AdvisoryWorkflowAppliedAction {
    param($Assessment, [Parameter(Mandatory = $true)]$Applied)
    $existing = @((Get-AdvisoryWorkflowProperty -Record $Assessment -Name 'applied_actions'))
    $existing += $Applied
    if ($null -eq $Assessment.PSObject.Properties['applied_actions']) {
        $Assessment | Add-Member -NotePropertyName applied_actions -NotePropertyValue @($existing)
    } else {
        $Assessment.applied_actions = @($existing)
    }
    return $Assessment
}

function Get-AdvisoryWorkflowIssueFingerprint {
    param($Request, $Findings, $Decisions)
    $content = [pscustomobject]@{
        findings  = @($Findings)
        decisions = @($Decisions)
    }
    return Get-AdvisoryWorkflowSha256 -Text (ConvertTo-AdvisoryWorkflowCanonicalJson -Value ([pscustomobject]@{
                action                 = 'issue'
                target                 = [string](Get-AdvisoryWorkflowProperty -Record $Request -Name 'Target')
                target_reference       = [string](Get-AdvisoryWorkflowProperty -Record $Request -Name 'TargetReference')
                review_question        = [string](Get-AdvisoryWorkflowProperty -Record $Request -Name 'ReviewQuestion')
                evidence_boundary      = [string](Get-AdvisoryWorkflowProperty -Record $Request -Name 'EvidenceBoundary')
                intentional_latitude   = [string](Get-AdvisoryWorkflowProperty -Record $Request -Name 'IntentionalLatitude')
                reviewer_provenance    = [string](Get-AdvisoryWorkflowProperty -Record $Request -Name 'ReviewerProvenance')
                advisory_readiness     = [string](Get-AdvisoryWorkflowProperty -Record $Request -Name 'AdvisoryReadiness')
                readiness_reason       = [string](Get-AdvisoryWorkflowProperty -Record $Request -Name 'ReadinessReason')
                report_content         = (Get-AdvisoryWorkflowSha256 -Text (ConvertTo-AdvisoryWorkflowCanonicalJson -Value $content))
            }))
}

function Get-AdvisoryWorkflowMutationFingerprint {
    param($Request, [Parameter(Mandatory = $true)][string]$ActionName)
    $decision = ConvertFrom-AdvisoryWorkflowJson -Text ([string](Get-AdvisoryWorkflowProperty -Record $Request -Name 'DecisionJson'))
    $turn = ConvertFrom-AdvisoryWorkflowJson -Text ([string](Get-AdvisoryWorkflowProperty -Record $Request -Name 'TurnContextJson'))
    return Get-AdvisoryWorkflowSha256 -Text (ConvertTo-AdvisoryWorkflowCanonicalJson -Value ([pscustomobject]@{
                action                     = $ActionName
                report_id                  = [string](Get-AdvisoryWorkflowProperty -Record $Request -Name 'ReportId')
                finding_id                 = [string](Get-AdvisoryWorkflowProperty -Record $Request -Name 'FindingId')
                prior_event_id             = [string](Get-AdvisoryWorkflowProperty -Record $Request -Name 'PriorEventId')
                selected_work_destination  = [string](Get-AdvisoryWorkflowProperty -Record $Request -Name 'SelectedWorkDestination')
                next_owner                 = [string](Get-AdvisoryWorkflowProperty -Record $Request -Name 'NextOwner')
                amendment_reason           = [string](Get-AdvisoryWorkflowProperty -Record $Request -Name 'AmendmentReason')
                slate_reply                = [string](Get-AdvisoryWorkflowProperty -Record $Request -Name 'SlateReply')
                decision                   = [string](Get-AdvisoryWorkflowProperty -Record $decision -Name 'decision')
                decision_status            = [string](Get-AdvisoryWorkflowProperty -Record $decision -Name 'status')
                decision_rationale         = [string](Get-AdvisoryWorkflowProperty -Record $decision -Name 'rationale')
                turn_state                 = [string](Get-AdvisoryWorkflowProperty -Record $turn -Name 'turn_state')
            }))
}

function New-AdvisoryWorkflowResult {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Returns an in-memory result envelope. It does not change system state.')]
    param([hashtable]$Fields)
    $ordered = [ordered]@{}
    foreach ($key in @($Fields.Keys)) { $ordered[$key] = $Fields[$key] }
    return [pscustomobject]$ordered
}

function Get-AdvisoryWorkflowIndexStatus {
    param(
        [Parameter(Mandatory = $true)][string]$FeatureRoot,
        [Parameter(Mandatory = $true)][string]$Feature
    )
    $path = Get-AdvisoryWorkflowIndexPath -FeatureRoot $FeatureRoot
    if (-not (Test-Path -LiteralPath $path)) { return 'not_requested' }
    $validity = Test-AdvisoryWorkflowIndexValid -FeatureRoot $FeatureRoot -Feature $Feature
    if ($validity.valid) { return 'current' }
    return 'stale'
}

function New-AdvisoryWorkflowRecordedResult {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Returns an in-memory recorded result. It does not change system state.')]
    param(
        $RecordedEvent,
        [string]$ActionName,
        [string]$ActionId,
        [string]$ReportId,
        [string]$ReportPath,
        [string]$FindingId = '',
        [string]$PriorEventId = '',
        $TurnContext = $null,
        [string]$TurnCorrelation = '',
        $Route = $null,
        [string]$IndexStatus,
        [string[]]$RecordedFindings = @(),
        [string[]]$NeedsOwner = @()
    )
    $fields = @{
        status        = 'recorded'
        action_id     = $ActionId
        action        = $ActionName
        action_class  = (Get-AdvisoryActionClass -Action $ActionName)
        report_id     = $ReportId
        report_path   = $ReportPath
        event_id      = [string](Get-AdvisoryWorkflowProperty -Record $RecordedEvent -Name 'event_id')
        event_kind    = $script:AdvisoryWorkflowExcludedEventKind
        event_action  = $ActionName
        index_status  = $IndexStatus
        gate_effect   = 'none'
        mutates_turn  = $false
    }
    if (-not (Test-AdvisoryWorkflowBlank $FindingId)) { $fields.finding_id = $FindingId }
    if (-not (Test-AdvisoryWorkflowBlank $PriorEventId)) { $fields.prior_event_id = $PriorEventId }
    if (-not (Test-AdvisoryWorkflowBlank $TurnCorrelation)) { $fields.turn_correlation = $TurnCorrelation }
    if ($null -ne $TurnContext) { $fields.turn_context = $TurnContext }
    if (@($RecordedFindings).Count -gt 0) { $fields.recorded_findings = @($RecordedFindings) }
    if (@($NeedsOwner).Count -gt 0) { $fields.needs_owner = @($NeedsOwner) }
    if ($null -ne $Route) {
        $fields.mutates_turn = $false
        $fields.mutates_phase = $false
        $fields.changes_ownership = $false
        $fields.changes_mandatory_next_action = $false
        $fields.selected_work_destination = [string](Get-AdvisoryWorkflowProperty -Record $Route -Name 'selected_work_destination')
        $fields.next_owner = [string](Get-AdvisoryWorkflowProperty -Record $Route -Name 'next_owner')
    }
    return New-AdvisoryWorkflowResult -Fields $fields
}

function New-AdvisoryWorkflowDuplicateFromEvent {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Returns an in-memory duplicate result. It does not change system state.')]
    param($Stored, [string]$IndexStatus)
    $payload = $Stored.payload
    $evt = $Stored.event
    $fields = @{
        status       = 'duplicate'
        action_id    = [string](Get-AdvisoryWorkflowProperty -Record $payload -Name 'action_id')
        action       = [string](Get-AdvisoryWorkflowProperty -Record $payload -Name 'action')
        report_id    = [string](Get-AdvisoryWorkflowProperty -Record $payload -Name 'report_id')
        report_path  = [string](Get-AdvisoryWorkflowProperty -Record $payload -Name 'report_path')
        event_id     = [string](Get-AdvisoryWorkflowProperty -Record $evt -Name 'event_id')
        event_kind   = $script:AdvisoryWorkflowExcludedEventKind
        event_action = [string](Get-AdvisoryWorkflowProperty -Record $payload -Name 'action')
        index_status = $IndexStatus
        gate_effect  = 'none'
    }
    $findingId = [string](Get-AdvisoryWorkflowProperty -Record $payload -Name 'finding_id')
    $prior = [string](Get-AdvisoryWorkflowProperty -Record $payload -Name 'prior_event_id')
    if (-not (Test-AdvisoryWorkflowBlank $findingId)) { $fields.finding_id = $findingId }
    if (-not (Test-AdvisoryWorkflowBlank $prior)) { $fields.prior_event_id = $prior }
    return New-AdvisoryWorkflowResult -Fields $fields
}

function New-AdvisoryWorkflowFailure {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Returns an in-memory failure result. It does not change system state.')]
    param(
        [Parameter(Mandatory = $true)][string]$Status,
        [Parameter(Mandatory = $true)][string]$Code,
        [string]$Action = '',
        [string]$ActionId = '',
        [string]$EventId = '',
        [string]$Message = '',
        [object[]]$Diagnostics = @(),
        [string[]]$NeedsOwner,
        [string[]]$RecordedFindings,
        [string]$ActionClass = ''
    )
    $fields = @{
        status          = $Status
        action          = $Action
        action_id       = $ActionId
        event_id        = $EventId
        diagnostic_code = $Code
        diagnostic      = $Message
        diagnostics     = @($Diagnostics)
        gate_effect     = 'none'
    }
    if ($PSBoundParameters.ContainsKey('NeedsOwner')) { $fields.needs_owner = @($NeedsOwner) }
    if ($PSBoundParameters.ContainsKey('RecordedFindings')) { $fields.recorded_findings = @($RecordedFindings) }
    if (-not [string]::IsNullOrWhiteSpace($ActionClass)) { $fields.action_class = $ActionClass }
    return New-AdvisoryWorkflowResult -Fields $fields
}

function Resolve-AdvisoryWorkflowIdempotency {
    param(
        [Parameter(Mandatory = $true)][string]$FeatureRoot,
        [Parameter(Mandatory = $true)][string]$Feature,
        [Parameter(Mandatory = $true)][string]$ActionId,
        [Parameter(Mandatory = $true)][string]$Fingerprint,
        [Parameter(Mandatory = $true)][string]$ActionName,
        [switch]$RequireReport
    )
    $stored = Find-AdvisoryWorkflowStoredAction -FeatureRoot $FeatureRoot -ActionId $ActionId
    if ($null -eq $stored) { return $null }
    if ($stored.source -eq 'receipt') {
        $receiptFingerprint = [string](Get-AdvisoryWorkflowProperty -Record $stored.receipt -Name 'fingerprint')
        $receiptAction = [string](Get-AdvisoryWorkflowProperty -Record $stored.receipt -Name 'action')
        if ($receiptAction -ne $ActionName -or $receiptFingerprint -ne $Fingerprint) {
            return New-AdvisoryWorkflowFailure -Status 'rejected' -Code 'ACTION_ID_CONFLICT' -Action $ActionName -ActionId $ActionId -Message 'ACTION_ID_CONFLICT'
        }
        $original = Get-AdvisoryWorkflowProperty -Record $stored.receipt -Name 'result'
        $copy = [ordered]@{}
        foreach ($property in @($original.PSObject.Properties)) { $copy[$property.Name] = $property.Value }
        $copy['status'] = 'duplicate'
        return [pscustomobject]$copy
    }
    $payload = $stored.payload
    $storedFingerprint = [string](Get-AdvisoryWorkflowProperty -Record $payload -Name 'canonical_fingerprint')
    $storedAction = [string](Get-AdvisoryWorkflowProperty -Record $payload -Name 'action')
    if ($storedAction -ne $ActionName -or $storedFingerprint -ne $Fingerprint) {
        return New-AdvisoryWorkflowFailure -Status 'rejected' -Code 'ACTION_ID_CONFLICT' -Action $ActionName -ActionId $ActionId `
            -EventId ([string](Get-AdvisoryWorkflowProperty -Record $stored.event -Name 'event_id')) -Message 'ACTION_ID_CONFLICT'
    }
    $reportPath = [string](Get-AdvisoryWorkflowProperty -Record $payload -Name 'report_path')
    $fileName = Split-Path -Leaf ($reportPath -replace '/', '\')
    $fullPath = Join-Path (Get-AdvisoryWorkflowAdvisoryDirectory -FeatureRoot $FeatureRoot) $fileName
    if ($RequireReport -and -not (Test-Path -LiteralPath $fullPath)) {
        return New-AdvisoryWorkflowFailure -Status 'partial' -Code 'REPORT_MISSING' -Action $ActionName -ActionId $ActionId `
            -EventId ([string](Get-AdvisoryWorkflowProperty -Record $stored.event -Name 'event_id')) `
            -Message 'The continuity event exists and the report file is missing.' `
            -Diagnostics @((New-AdvisoryWorkflowDiagnostic -Path $reportPath -Code 'missing-reference' -ObservedProblem 'referenced report is missing' -Provenance 'journal'))
    }
    return New-AdvisoryWorkflowDuplicateFromEvent -Stored $stored -IndexStatus (Get-AdvisoryWorkflowIndexStatus -FeatureRoot $FeatureRoot -Feature $Feature)
}

function Find-AdvisoryWorkflowAppliedReport {
    param(
        [Parameter(Mandatory = $true)][string]$FeatureRoot,
        [Parameter(Mandatory = $true)][string]$ActionId,
        [Parameter(Mandatory = $true)][string]$Fingerprint
    )
    foreach ($file in @(Get-AdvisoryWorkflowReportFile -FeatureRoot $FeatureRoot)) {
        try { $read = Read-AdvisoryWorkflowReportText -Path $file.FullName } catch { continue }
        $record = Get-AdvisoryWorkflowRecordFromText -Text $read.text
        if ($null -eq $record) { continue }
        foreach ($applied in @((Get-AdvisoryWorkflowProperty -Record $record -Name 'applied_actions'))) {
            if ($null -eq $applied) { continue }
            $appliedId = [string](Get-AdvisoryWorkflowProperty -Record $applied -Name 'action_id')
            $appliedFingerprint = [string](Get-AdvisoryWorkflowProperty -Record $applied -Name 'fingerprint')
            if (($appliedId -ceq $ActionId) -and ($appliedFingerprint -ceq $Fingerprint)) {
                return $record
            }
        }
    }
    return $null
}

function Find-AdvisoryWorkflowReportById {
    param(
        [Parameter(Mandatory = $true)][string]$FeatureRoot,
        [Parameter(Mandatory = $true)][string]$ReportId
    )
    foreach ($file in @(Get-AdvisoryWorkflowReportFile -FeatureRoot $FeatureRoot)) {
        try {
            $read = Read-AdvisoryWorkflowReportText -Path $file.FullName
        } catch {
            continue
        }
        $record = Get-AdvisoryWorkflowRecordFromText -Text $read.text
        if ($null -eq $record) { continue }
        if ([string](Get-AdvisoryWorkflowProperty -Record $record -Name 'report_id') -eq $ReportId) {
            return [pscustomobject]@{ file = $file; record = $record }
        }
    }
    return $null
}

function Get-AdvisoryWorkflowFilenameHistory {
    param(
        [Parameter(Mandatory = $true)][string]$FeatureRoot,
        [Parameter(Mandatory = $true)][string]$Feature,
        [Parameter(Mandatory = $true)][string]$Target
    )
    $history = @()
    foreach ($file in @(Get-AdvisoryWorkflowReportFile -FeatureRoot $FeatureRoot)) {
        $match = [regex]::Match($file.Name, $script:AdvisoryWorkflowReportNamePattern)
        if (-not $match.Success) { continue }
        if ($match.Groups[1].Value -ne $Target) { continue }
        $history += [pscustomobject]@{
            feature   = $Feature
            target    = $Target
            iteration = [int]$match.Groups[2].Value
        }
    }
    return @($history)
}

function Invoke-AdvisoryWorkflowIssue {
    param($Request, [string]$Fingerprint)
    $featureRoot = [string]$Request.FeatureRoot
    $feature = [string]$Request.Feature
    $actionId = [string]$Request.ActionId
    $findings = ConvertFrom-AdvisoryWorkflowJsonArray -Text ([string]$Request.FindingsJson)
    $decisions = ConvertFrom-AdvisoryWorkflowJsonArray -Text ([string]$Request.DecisionsJson)
    $idem = Resolve-AdvisoryWorkflowIdempotency -FeatureRoot $featureRoot -Feature $feature -ActionId $actionId `
        -Fingerprint $Fingerprint -ActionName 'issue' -RequireReport
    if ($null -ne $idem) { return $idem }
    $appliedReport = Find-AdvisoryWorkflowAppliedReport -FeatureRoot $featureRoot -ActionId $actionId -Fingerprint $Fingerprint
    if ($null -ne $appliedReport) {
        $evt = Add-AdvisoryContinuityJournalEvent -FeatureRoot $featureRoot -Feature $feature -Action Issue -Assessment $appliedReport `
            -ActorName ([string]$Request.Actor) -OccurredAt ([string]$Request.OccurredAt) -ActionId $actionId `
            -CanonicalFingerprint $Fingerprint -CommandName 'harness-advisory.ps1'
        return New-AdvisoryWorkflowRecordedResult -RecordedEvent $evt -ActionName 'issue' -ActionId $actionId `
            -ReportId ([string]$appliedReport.report_id) -ReportPath ([string]$appliedReport.relative_path) `
            -IndexStatus (Get-AdvisoryWorkflowIndexStatus -FeatureRoot $featureRoot -Feature $feature)
    }
    foreach ($name in @('Target', 'TargetReference', 'ReviewQuestion', 'EvidenceBoundary', 'IntentionalLatitude', 'ReviewerProvenance', 'AdvisoryReadiness', 'ReadinessReason', 'Actor')) {
        if (Test-AdvisoryWorkflowBlank (Get-AdvisoryWorkflowProperty -Record $Request -Name $name)) {
            return New-AdvisoryWorkflowFailure -Status 'failed' -Code 'MISSING_INPUT' -Action 'issue' -ActionId $actionId -Message "$name is required."
        }
    }
    $history = @(Get-AdvisoryWorkflowFilenameHistory -FeatureRoot $featureRoot -Feature $feature -Target ([string]$Request.Target))
    $issuedAt = [string]$Request.IssuedAt
    $assessment = New-AdvisoryAssessment -FeatureSlug $feature -Target ([string]$Request.Target) `
        -TargetReference ([string]$Request.TargetReference) -ReviewQuestion ([string]$Request.ReviewQuestion) `
        -EvidenceBoundary ([string]$Request.EvidenceBoundary) -IntentionalLatitude ([string]$Request.IntentionalLatitude) `
        -ReviewerProvenance ([string]$Request.ReviewerProvenance) -AdvisoryReadiness ([string]$Request.AdvisoryReadiness) `
        -ReadinessReason ([string]$Request.ReadinessReason) -IssuedAt $issuedAt -History $history -Findings $findings -Decisions $decisions
    $assessment = Add-AdvisoryWorkflowAppliedAction -Assessment $assessment -Applied ([pscustomobject]@{
            action_id   = $actionId
            action      = 'issue'
            fingerprint = $Fingerprint
        })
    Save-AdvisoryWorkflowReport -FeatureRoot $featureRoot -Assessment $assessment | Out-Null
    $gate = Resolve-AdvisoryWorkflowTurnGate -Request $Request -ActionName 'issue' -ActionId $actionId
    if ($gate.rejected) { return $gate.result }
    $turnContext = $gate.turn_context
    $turnCorrelation = $gate.turn_correlation
    $evt = Add-AdvisoryContinuityJournalEvent -FeatureRoot $featureRoot -Feature $feature -Action Issue -Assessment $assessment `
        -ActorName ([string]$Request.Actor) -OccurredAt ([string]$Request.OccurredAt) -ActionId $actionId `
        -CanonicalFingerprint $Fingerprint -CommandName 'harness-advisory.ps1' -TurnContext $turnContext -TurnCorrelation $turnCorrelation
    return New-AdvisoryWorkflowRecordedResult -RecordedEvent $evt -ActionName 'issue' -ActionId $actionId `
        -ReportId ([string]$assessment.report_id) -ReportPath ([string]$assessment.relative_path) `
        -TurnContext $turnContext -TurnCorrelation $turnCorrelation `
        -IndexStatus (Get-AdvisoryWorkflowIndexStatus -FeatureRoot $featureRoot -Feature $feature)
}

function Resolve-AdvisoryWorkflowRecordedChange {
    param(
        $Request,
        [string]$Fingerprint,
        [string]$ActionName,
        [string]$ReportId
    )
    $featureRoot = [string]$Request.FeatureRoot
    $feature = [string]$Request.Feature
    $actionId = [string]$Request.ActionId
    $idem = Resolve-AdvisoryWorkflowIdempotency -FeatureRoot $featureRoot -Feature $feature -ActionId $actionId `
        -Fingerprint $Fingerprint -ActionName $ActionName -RequireReport
    if ($null -ne $idem) {
        return [pscustomobject]@{ done = $true; result = $idem; record = $null }
    }
    $found = Find-AdvisoryWorkflowReportById -FeatureRoot $featureRoot -ReportId $ReportId
    if ($null -eq $found) {
        return [pscustomobject]@{
            done   = $true
            record = $null
            result = (New-AdvisoryWorkflowFailure -Status 'failed' -Code 'REPORT_MISSING' -Action $ActionName -ActionId $actionId -Message 'The advisory report was not found.')
        }
    }
    return [pscustomobject]@{ done = $false; result = $null; record = $found.record }
}

function Complete-AdvisoryWorkflowRecordedChange {
    param(
        $Request,
        [string]$Fingerprint,
        [Parameter(Mandatory = $true)][hashtable]$Change
    )
    $actionName = [string]$Change['ActionName']
    $reportId = [string]$Change['ReportId']
    $findingId = [string]$Change['FindingId']
    $assessment = $Change['Assessment']
    $journalArgs = $Change['JournalArgs']
    $route = $Change['Route']
    $recordedFindings = @()
    if ($Change.ContainsKey('RecordedFindings') -and $null -ne $Change['RecordedFindings']) {
        $recordedFindings = @($Change['RecordedFindings'])
    }
    $needsOwner = @()
    if ($Change.ContainsKey('NeedsOwner') -and $null -ne $Change['NeedsOwner']) {
        $needsOwner = @($Change['NeedsOwner'])
    }
    $featureRoot = [string]$Request.FeatureRoot
    $feature = [string]$Request.Feature
    $actionId = [string]$Request.ActionId
    $gate = Resolve-AdvisoryWorkflowTurnGate -Request $Request -ActionName $actionName -ActionId $actionId
    if ($gate.rejected) { return $gate.result }
    $turnContext = $gate.turn_context
    $turnCorrelation = $gate.turn_correlation
    if ($Change.ContainsKey('TurnContext') -and $null -ne $Change['TurnContext']) { $turnContext = $Change['TurnContext'] }
    if ($Change.ContainsKey('TurnCorrelation') -and -not (Test-AdvisoryWorkflowBlank ([string]$Change['TurnCorrelation']))) {
        $turnCorrelation = [string]$Change['TurnCorrelation']
    }
    $assessment = Add-AdvisoryWorkflowAppliedAction -Assessment $assessment -Applied ([pscustomobject]@{
            action_id = $actionId; action = $actionName; fingerprint = $Fingerprint
        })
    Save-AdvisoryWorkflowReport -FeatureRoot $featureRoot -Assessment $assessment | Out-Null
    $journalAction = [cultureinfo]::InvariantCulture.TextInfo.ToTitleCase($actionName)
    if ($null -eq $journalArgs) { $journalArgs = @{} }
    if (-not $journalArgs.ContainsKey('TurnContext')) { $journalArgs['TurnContext'] = $turnContext }
    if (-not $journalArgs.ContainsKey('TurnCorrelation')) { $journalArgs['TurnCorrelation'] = $turnCorrelation }
    $evt = Add-AdvisoryContinuityJournalEvent -FeatureRoot $featureRoot -Feature $feature -Action $journalAction -Assessment $assessment `
        -ActorName ([string]$Request.Actor) -OccurredAt ([string]$Request.OccurredAt) -ActionId $actionId `
        -CanonicalFingerprint $Fingerprint -CommandName 'harness-advisory.ps1' @journalArgs
    return New-AdvisoryWorkflowRecordedResult -RecordedEvent $evt -ActionName $actionName -ActionId $actionId -ReportId $reportId `
        -ReportPath ([string]$assessment.relative_path) -FindingId $findingId `
        -PriorEventId ([string](Get-AdvisoryWorkflowProperty -Record $Request -Name 'PriorEventId')) `
        -TurnContext $turnContext -TurnCorrelation $turnCorrelation -Route $route `
        -IndexStatus (Get-AdvisoryWorkflowIndexStatus -FeatureRoot $featureRoot -Feature $feature) `
        -RecordedFindings $recordedFindings -NeedsOwner $needsOwner
}

function Invoke-AdvisoryWorkflowDecision {
    param($Request, [string]$Fingerprint)
    $actionId = [string]$Request.ActionId
    $reportId = [string]$Request.ReportId
    $findingId = [string]$Request.FindingId
    $slateReply = [string](Get-AdvisoryWorkflowProperty -Record $Request -Name 'SlateReply')
    $decision = ConvertFrom-AdvisoryWorkflowJson -Text ([string]$Request.DecisionJson)
    if ((Test-AdvisoryWorkflowBlank $reportId)) {
        return New-AdvisoryWorkflowFailure -Status 'failed' -Code 'MISSING_INPUT' -Action 'decision' -ActionId $actionId -Message 'decision requires report_id.'
    }
    if ((Test-AdvisoryWorkflowBlank $slateReply) -and ((Test-AdvisoryWorkflowBlank $findingId) -or ($null -eq $decision))) {
        return New-AdvisoryWorkflowFailure -Status 'failed' -Code 'MISSING_INPUT' -Action 'decision' -ActionId $actionId -Message 'decision requires report_id, finding_id, and decision fields.'
    }
    $start = Resolve-AdvisoryWorkflowRecordedChange -Request $Request -Fingerprint $Fingerprint -ActionName 'decision' -ReportId $reportId
    if ($start.done) { return $start.result }

    if (-not (Test-AdvisoryWorkflowBlank $slateReply)) {
        $bound = ConvertFrom-AdvisoryDecisionSlate -SlateReply $slateReply -Assessment $start.record
        if (-not $bound.direction_satisfied) {
            return New-AdvisoryWorkflowFailure -Status 'needs_direction' -Code 'MISSING_DEFER_DIRECTION' -Action 'decision' -ActionId $actionId `
                -Message 'Defer, Promote To Backlog, and More Info require a direction or trigger.' `
                -Diagnostics @($bound.needs_direction)
        }
        if (@($bound.rows).Count -eq 0 -and @($bound.needs_owner).Count -gt 0) {
            return New-AdvisoryWorkflowFailure -Status 'needs_owner' -Code 'MISSING_FIX_OWNER' -Action 'decision' -ActionId $actionId `
                -Message 'A Fix on a route with no default owner requires a named owner.' `
                -NeedsOwner @($bound.needs_owner) -RecordedFindings @() -ActionClass (Get-AdvisoryActionClass -Action 'decision')
        }
        if (@($bound.rows).Count -eq 0) {
            return New-AdvisoryWorkflowFailure -Status 'failed' -Code 'MISSING_INPUT' -Action 'decision' -ActionId $actionId `
                -Message 'The slate bound no decisions.'
        }
        $assessment = $start.record
        $recorded = @()
        foreach ($row in @($bound.rows)) {
            $rowDecision = [pscustomobject]@{
                finding_id                    = [string]$row.finding_id
                decision                      = [string]$row.decision
                decided_by                    = 'operator'
                decided_at                    = $(if (Test-AdvisoryWorkflowBlank ([string]$Request.OccurredAt)) { (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') } else { [string]$Request.OccurredAt })
                rationale                     = [string]$row.rationale
                selected_work_destination     = [string]$row.selected_work_destination
                next_owner                    = [string]$row.next_owner
                trigger_or_needed_information = [string]$row.trigger_or_needed_information
                destination_reference         = 'advisory-operator-surface'
                status                        = [string]$row.status
            }
            $assessment = Update-AdvisoryAssessmentDecision -Assessment $assessment -Decision $rowDecision -FindingId ([string]$row.finding_id)
            $recorded += [string]$row.finding_id
        }
        $firstFinding = if ($recorded.Count -gt 0) { [string]$recorded[0] } else { '' }
        return Complete-AdvisoryWorkflowRecordedChange -Request $Request -Fingerprint $Fingerprint -Change @{
            ActionName        = 'decision'
            ReportId          = $reportId
            FindingId         = $firstFinding
            Assessment        = $assessment
            JournalArgs       = @{
                FindingId                 = $firstFinding
                PriorEventId              = [string]$Request.PriorEventId
                SelectedWorkDestination   = ''
            }
            RecordedFindings  = $recorded
            NeedsOwner        = @($bound.needs_owner)
        }
    }

    $assessment = Update-AdvisoryAssessmentDecision -Assessment $start.record -Decision $decision -FindingId $findingId
    return Complete-AdvisoryWorkflowRecordedChange -Request $Request -Fingerprint $Fingerprint -Change @{
        ActionName       = 'decision'
        ReportId         = $reportId
        FindingId        = $findingId
        Assessment       = $assessment
        JournalArgs      = @{
            FindingId               = $findingId
            PriorEventId            = [string]$Request.PriorEventId
            SelectedWorkDestination = [string]$Request.SelectedWorkDestination
        }
        RecordedFindings = @($findingId)
    }
}

function Invoke-AdvisoryWorkflowAmendment {
    param($Request, [string]$Fingerprint)
    $actionId = [string]$Request.ActionId
    $reportId = [string]$Request.ReportId
    $findingId = [string]$Request.FindingId
    $decision = ConvertFrom-AdvisoryWorkflowJson -Text ([string]$Request.DecisionJson)
    $reason = [string]$Request.AmendmentReason
    $prior = [string]$Request.PriorEventId
    if ((Test-AdvisoryWorkflowBlank $reportId) -or (Test-AdvisoryWorkflowBlank $findingId) -or ($null -eq $decision) -or (Test-AdvisoryWorkflowBlank $reason) -or (Test-AdvisoryWorkflowBlank $prior)) {
        return New-AdvisoryWorkflowFailure -Status 'failed' -Code 'MISSING_INPUT' -Action 'amendment' -ActionId $actionId -Message 'amendment requires report, finding, replacement decision, reason, and prior decision event.'
    }
    $amendedAt = [string]$Request.AmendedAt
    if (Test-AdvisoryWorkflowBlank $amendedAt) { $amendedAt = [string]$Request.OccurredAt }
    $start = Resolve-AdvisoryWorkflowRecordedChange -Request $Request -Fingerprint $Fingerprint -ActionName 'amendment' -ReportId $reportId
    if ($start.done) { return $start.result }
    $assessment = Add-AdvisoryDecisionAmendment -Assessment $start.record -FindingId $findingId -Decision $decision `
        -Reason $reason -Author ([string]$Request.Actor) -AmendedAt $amendedAt
    return Complete-AdvisoryWorkflowRecordedChange -Request $Request -Fingerprint $Fingerprint -Change @{
        ActionName  = 'amendment'
        ReportId    = $reportId
        FindingId   = $findingId
        Assessment  = $assessment
        JournalArgs = @{
            FindingId    = $findingId
            PriorEventId = $prior
            Reason       = $reason
        }
    }
}

function Invoke-AdvisoryWorkflowRoute {
    param($Request, [string]$Fingerprint)
    $actionId = [string]$Request.ActionId
    $reportId = [string]$Request.ReportId
    $findingId = [string]$Request.FindingId
    $prior = [string]$Request.PriorEventId
    if ((Test-AdvisoryWorkflowBlank $reportId) -or (Test-AdvisoryWorkflowBlank $findingId) -or (Test-AdvisoryWorkflowBlank $prior)) {
        return New-AdvisoryWorkflowFailure -Status 'failed' -Code 'MISSING_INPUT' -Action 'route' -ActionId $actionId -Message 'route requires report, finding, and source decision event.'
    }
    $mandatory = [string]$Request.MandatoryNextAction
    $start = Resolve-AdvisoryWorkflowRecordedChange -Request $Request -Fingerprint $Fingerprint -ActionName 'route' -ReportId $reportId
    if ($start.done) { return $start.result }
    # Turn agreement is enforced once in Complete-AdvisoryWorkflowRecordedChange.
    $derived = Resolve-AdvisoryWorkflowTurnCorrelation -FeatureRoot ([string]$Request.FeatureRoot)
    $turn = Get-AdvisoryWorkflowProperty -Record $derived -Name 'turn_context'
    $turnCorrelation = [string](Get-AdvisoryWorkflowProperty -Record $derived -Name 'turn_correlation')
    $route = New-AdvisoryRouteCorrelation -Assessment $start.record -FindingId $findingId -TurnContext $turn `
        -MandatoryNextAction $mandatory -SelectedWorkDestination ([string]$Request.SelectedWorkDestination)
    $assessment = Set-AdvisoryAssessmentDecisionRouted -Assessment $start.record -FindingId $findingId -Route $route
    $destination = [string]$Request.SelectedWorkDestination
    if (Test-AdvisoryWorkflowBlank $destination) { $destination = [string]$route.selected_work_destination }
    return Complete-AdvisoryWorkflowRecordedChange -Request $Request -Fingerprint $Fingerprint -Change @{
        ActionName       = 'route'
        ReportId         = $reportId
        FindingId        = $findingId
        Assessment       = $assessment
        TurnContext      = $turn
        TurnCorrelation  = $turnCorrelation
        Route            = $route
        JournalArgs      = @{
            FindingId                 = $findingId
            PriorEventId              = $prior
            TurnContext               = $turn
            TurnCorrelation           = $turnCorrelation
            SelectedWorkDestination   = $destination
            MandatoryNextAction       = $mandatory
        }
    }
}

function Get-AdvisoryWorkflowJournalFingerprint {
    param([Parameter(Mandatory = $true)][string]$FeatureRoot)
    $path = Join-Path $FeatureRoot 'journal\events.jsonl'
    if (-not (Test-Path -LiteralPath $path)) { return (Get-AdvisoryWorkflowSha256 -Text '') }
    return Get-AdvisoryWorkflowFileSha256 -Path $path
}

. (Join-Path $PSScriptRoot 'AdvisoryReportScan.ps1')
. (Join-Path $PSScriptRoot 'AdvisoryIndexRebuild.ps1')

function Invoke-AdvisoryWorkflowRebuild {
    param($Request)
    $featureRoot = [string]$Request.FeatureRoot
    $feature = [string]$Request.Feature
    $actionId = [string]$Request.ActionId
    $scope = [string]$Request.ReportScope
    $fingerprint = Get-AdvisoryWorkflowSha256 -Text (ConvertTo-AdvisoryWorkflowCanonicalJson -Value ([pscustomobject]@{
                action = 'rebuild-index'; feature = $feature; report_scope = $scope
            }))
    $stored = Find-AdvisoryWorkflowStoredAction -FeatureRoot $featureRoot -ActionId $actionId
    if ($null -ne $stored) {
        if ($stored.source -eq 'receipt') {
            $receiptFingerprint = [string](Get-AdvisoryWorkflowProperty -Record $stored.receipt -Name 'fingerprint')
            if ($receiptFingerprint -ne $fingerprint -or [string](Get-AdvisoryWorkflowProperty -Record $stored.receipt -Name 'action') -ne 'rebuild-index') {
                return New-AdvisoryWorkflowFailure -Status 'rejected' -Code 'ACTION_ID_CONFLICT' -Action 'rebuild-index' -ActionId $actionId -Message 'ACTION_ID_CONFLICT'
            }
            $original = Get-AdvisoryWorkflowProperty -Record $stored.receipt -Name 'result'
            $copy = [ordered]@{}
            foreach ($property in @($original.PSObject.Properties)) {
                if ($property.Name -eq 'full_path') { continue }
                $copy[$property.Name] = $property.Value
            }
            $copy['status'] = 'duplicate'
            $copy['action_id'] = $actionId
            return [pscustomobject]$copy
        }
        return New-AdvisoryWorkflowFailure -Status 'rejected' -Code 'ACTION_ID_CONFLICT' -Action 'rebuild-index' -ActionId $actionId `
            -EventId ([string](Get-AdvisoryWorkflowProperty -Record $stored.event -Name 'event_id')) -Message 'ACTION_ID_CONFLICT'
    }
    $lastValid = Get-AdvisoryWorkflowLastValidFingerprint -FeatureRoot $featureRoot
    $beforeHash = $null
    $indexPath = Get-AdvisoryWorkflowIndexPath -FeatureRoot $featureRoot
    if (Test-Path -LiteralPath $indexPath) { $beforeHash = Get-AdvisoryWorkflowFileSha256 -Path $indexPath }
    $core = Invoke-AdvisoryWorkflowRebuildCore -FeatureRoot $featureRoot -Feature $feature -ReportScope $scope
    if (-not $core.ok) {
        $afterHash = $null
        if (Test-Path -LiteralPath $indexPath) { $afterHash = Get-AdvisoryWorkflowFileSha256 -Path $indexPath }
        if (($null -ne $beforeHash) -and ($beforeHash -ne $afterHash)) {
            throw 'Failed rebuild changed the prior advisory index.'
        }
        $fields = @{
            status                     = 'failed'
            action                     = 'rebuild-index'
            action_id                  = $actionId
            index_path                 = (Get-AdvisoryWorkflowRepoRelativeIndexPath -Feature $feature)
            source_journal_fingerprint = [string]$core.journal_fingerprint
            diagnostic_count           = 1
            diagnostic_code            = 'UNREADABLE_REPORT'
            diagnostics                = @($core.diagnostic)
            gate_effect                = 'none'
        }
        if (-not (Test-AdvisoryWorkflowBlank $lastValid)) { $fields.last_valid_index_fingerprint = $lastValid }
        return New-AdvisoryWorkflowResult -Fields $fields
    }
    $result = $core.result
    $result | Add-Member -NotePropertyName action_id -NotePropertyValue $actionId -Force
    Save-AdvisoryWorkflowReceipt -FeatureRoot $featureRoot -Receipt ([pscustomobject]@{
            action_id   = $actionId
            action      = 'rebuild-index'
            fingerprint = $fingerprint
            result      = $result
        })
    return $result
}

function New-AdvisoryWorkflowResumeContext {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Returns an in-memory resume context. It does not change system state.')]
    param($Report, [string[]]$SourceEventIds, [string]$MandatoryNextAction)
    if ($null -eq $Report) { return $null }
    $presentation = Get-AdvisoryResumePresentation -MandatoryNextAction $MandatoryNextAction -AdvisoryReports @($Report)
    $unresolved = @()
    if ($null -ne $presentation.advisory_context) {
        $unresolved = @($presentation.advisory_context.unresolved_decisions)
    }
    return [pscustomobject]@{
        report_id            = [string](Get-AdvisoryWorkflowProperty -Record $Report -Name 'report_id')
        report_path          = [string](Get-AdvisoryWorkflowProperty -Record $Report -Name 'relative_path')
        source_event_ids     = @($SourceEventIds)
        unresolved_decisions = @($unresolved)
    }
}

function Get-AdvisoryWorkflowPairedReviewPath {
    param([Parameter(Mandatory = $true)][string]$FeatureRoot)
    $paths = @()
    $dir = Join-Path $FeatureRoot '.reviews'
    if (-not (Test-Path -LiteralPath $dir)) { return @() }
    foreach ($file in @(Get-ChildItem -LiteralPath $dir -File | Sort-Object Name)) {
        $paths += ('.reviews/' + $file.Name)
    }
    return @($paths)
}

function Invoke-AdvisoryWorkflowResume {
    param($Request)
    $featureRoot = [string]$Request.FeatureRoot
    $feature = [string]$Request.Feature
    $actionId = [string]$Request.ActionId
    $mandatory = [string]$Request.MandatoryNextAction
    if (Test-AdvisoryWorkflowBlank $mandatory) {
        return New-AdvisoryWorkflowFailure -Status 'failed' -Code 'MISSING_INPUT' -Action 'resume-context' -ActionId $actionId -Message 'resume requires the canonical mandatory next action.'
    }
    $stored = Find-AdvisoryWorkflowStoredAction -FeatureRoot $featureRoot -ActionId $actionId
    if ($null -ne $stored) {
        return New-AdvisoryWorkflowFailure -Status 'rejected' -Code 'ACTION_ID_CONFLICT' -Action 'resume-context' -ActionId $actionId -Message 'ACTION_ID_CONFLICT'
    }
    $paired = @(Get-AdvisoryWorkflowPairedReviewPath -FeatureRoot $featureRoot)
    $diagnostics = @()
    $steps = @('canonical-gate-discovery', 'canonical-paired-review-lookup')
    $validity = Test-AdvisoryWorkflowIndexValid -FeatureRoot $featureRoot -Feature $feature
    $indexStatus = 'absent'
    $rebuildAttempted = $false
    $selected = $null
    $sourceIds = @()
    if ($validity.valid) {
        $indexStatus = 'current'
        $steps += 'advisory-index'
        $selected = $null
        foreach ($report in @($validity.index.reports)) {
            $reportId = [string](Get-AdvisoryWorkflowProperty -Record $report -Name 'report_id')
            if ($reportId -eq [string]$validity.index.latest_report_id) {
                $found = Find-AdvisoryWorkflowReportById -FeatureRoot $featureRoot -ReportId $reportId
                if ($null -ne $found) { $selected = $found.record }
            }
        }
        $sourceIds = @($validity.index.source_event_ids)
    } else {
        if (Test-Path -LiteralPath (Get-AdvisoryWorkflowIndexPath -FeatureRoot $featureRoot)) { $indexStatus = 'stale' } else { $indexStatus = 'absent' }
        $rebuildAttempted = $true
        $core = Invoke-AdvisoryWorkflowRebuildCore -FeatureRoot $featureRoot -Feature $feature
        if ($core.ok) {
            $indexStatus = 'rebuilt'
            $steps += 'rebuild-index'
            $reloaded = Test-AdvisoryWorkflowIndexValid -FeatureRoot $featureRoot -Feature $feature
            if ($reloaded.valid -and -not (Test-AdvisoryWorkflowBlank $reloaded.index.latest_report_id)) {
                $found = Find-AdvisoryWorkflowReportById -FeatureRoot $featureRoot -ReportId ([string]$reloaded.index.latest_report_id)
                if ($null -ne $found) { $selected = $found.record }
                $sourceIds = @($reloaded.index.source_event_ids)
            }
            foreach ($diagnostic in @($reloaded.index.diagnostics)) { $diagnostics += $diagnostic }
        } else {
            $diagnostics += $core.diagnostic
            $steps += 'authoritative-fallback'
            $scan = $null
            try {
                $scan = Read-AdvisoryWorkflowScan -FeatureRoot $featureRoot -Feature $feature
            } catch {
                $scan = $null
            }
            if ($null -ne $scan) {
                $diagnostics += @($scan.diagnostics)
                if (@($scan.valid).Count -gt 0) {
                    $selected = Select-AdvisoryLatestReport -History $scan.valid
                }
            }
            $eventIds = @()
            foreach ($evt in @(Get-AdvisoryWorkflowContinuityEvent -FeatureRoot $featureRoot)) {
                $eventIds += [string](Get-AdvisoryWorkflowProperty -Record $evt -Name 'event_id')
            }
            $sourceIds = @($eventIds)
            $indexStatus = 'fallback'
        }
    }
    if (($null -ne $selected) -and (@($sourceIds).Count -gt 0)) {
        $known = @{}
        foreach ($evt in @(Get-AdvisoryWorkflowJournalEvent -FeatureRoot $featureRoot)) {
            $known[[string](Get-AdvisoryWorkflowProperty -Record $evt -Name 'event_id')] = $true
        }
        $kept = @()
        foreach ($eventId in @($sourceIds)) {
            if ($known.ContainsKey([string]$eventId)) { $kept += [string]$eventId }
            else {
                $diagnostics += New-AdvisoryWorkflowDiagnostic -Path '' -Code 'missing-reference' -ObservedProblem 'referenced journal event is missing' -Provenance 'advisory-index'
            }
        }
        $sourceIds = @($kept)
    }
    $context = New-AdvisoryWorkflowResumeContext -Report $selected -SourceEventIds $sourceIds -MandatoryNextAction $mandatory
    $steps += 'mandatory-next-action'
    $latestReportId = $null
    if ($null -ne $selected) {
        $latestReportId = [string](Get-AdvisoryWorkflowProperty -Record $selected -Name 'report_id')
    } elseif ($validity.valid) {
        $latestReportId = [string](Get-AdvisoryWorkflowProperty -Record $validity.index -Name 'latest_report_id')
    }
    return New-AdvisoryWorkflowResult -Fields @{
        status                         = 'resumed'
        action                         = 'resume-context'
        action_class                   = (Get-AdvisoryActionClass -Action 'resume-context')
        action_id                      = $actionId
        mandatory_next_action          = $mandatory
        changes_mandatory_next_action  = $false
        advisory_context               = $context
        latest_report_id               = $latestReportId
        index_schema_version           = 'advisory-index.v3'
        diagnostics                    = @($diagnostics)
        discovery_steps                = @($steps)
        paired_review_paths            = @($paired)
        index_status                   = $indexStatus
        prior_index_status             = $(if ($validity.valid) { 'current' } elseif (Test-Path -LiteralPath (Get-AdvisoryWorkflowIndexPath -FeatureRoot $featureRoot)) { 'stale' } else { 'absent' })
        staleness_reasons              = @($validity.reasons)
        rebuild_attempted              = $rebuildAttempted
        missing_advisory_blocks_resume = $false
        gate_effect                    = 'none'
    }
}

function Get-AdvisoryWorkflowProjectionFingerprint {
    param([Parameter(Mandatory = $true)][string]$FeatureRoot)
    $path = Join-Path $FeatureRoot '.review-loop.json'
    if (-not (Test-Path -LiteralPath $path)) { return (Get-AdvisoryWorkflowSha256 -Text '') }
    $loop = Get-Content -LiteralPath $path -Raw -Encoding UTF8 | ConvertFrom-Json
    if ($null -ne $loop.PSObject.Properties['updated']) { $loop.PSObject.Properties.Remove('updated') }
    return Get-AdvisoryWorkflowSha256 -Text (ConvertTo-AdvisoryWorkflowCanonicalJson -Value $loop)
}

function Test-AdvisoryWorkflowReviewLoopHasAdvisoryState {
    param([Parameter(Mandatory = $true)][string]$FeatureRoot, [string[]]$ReportIds = @())
    $path = Join-Path $FeatureRoot '.review-loop.json'
    if (-not (Test-Path -LiteralPath $path)) { return $false }
    $loop = Get-Content -LiteralPath $path -Raw -Encoding UTF8 | ConvertFrom-Json
    return Test-AdvisoryWorkflowProjectionValue -Value $loop -ReportIds $ReportIds
}

function Test-AdvisoryWorkflowProjectionValue {
    param($Value, [string[]]$ReportIds = @())
    if ($null -eq $Value) { return $false }
    if ($Value -is [string]) {
        if ($Value -ceq 'AdvisoryContinuityRecorded' -or $Value -ceq 'advisory-index' -or $Value -ceq 'advisory-index.v2' -or $Value -ceq 'advisory-index.v3' -or $Value -ceq 'advisory-index.jsonl') { return $true }
        foreach ($reportId in @($ReportIds)) {
            if ((-not (Test-AdvisoryWorkflowBlank $reportId)) -and ($Value -ceq $reportId)) { return $true }
        }
        return $false
    }
    if (($Value -is [System.ValueType]) -or ($Value -is [datetime])) { return $false }
    $isList = ($Value -is [System.Collections.IEnumerable]) -and (-not ($Value -is [pscustomobject]))
    if ($isList) {
        foreach ($item in @($Value)) {
            if (Test-AdvisoryWorkflowProjectionValue -Value $item -ReportIds $ReportIds) { return $true }
        }
        return $false
    }
    foreach ($property in @($Value.PSObject.Properties)) {
        if ($property.Name -ceq 'advisory' -or $property.Name -like 'advisory_*') { return $true }
        if (Test-AdvisoryWorkflowProjectionValue -Value $property.Value -ReportIds $ReportIds) { return $true }
    }
    return $false
}

function Invoke-AdvisoryWorkflowProjectionExclusion {
    param($Request)
    $featureRoot = [string]$Request.FeatureRoot
    $feature = [string]$Request.Feature
    $repoRoot = [string]$Request.RepoRoot
    $profileName = [string]$Request.ProfileName
    if (Test-AdvisoryWorkflowBlank $profileName) { $profileName = 'standard' }
    $events = @(Get-AdvisoryWorkflowJournalEvent -FeatureRoot $featureRoot)
    $advisory = @($events | Where-Object { [string](Get-AdvisoryWorkflowProperty -Record $_ -Name 'kind') -eq $script:AdvisoryWorkflowExcludedEventKind })
    $reportIds = @()
    foreach ($evt in @($advisory)) {
        $payload = Get-AdvisoryWorkflowProperty -Record $evt -Name 'payload'
        $reportIds += [string](Get-AdvisoryWorkflowProperty -Record $payload -Name 'report_id')
    }
    $tempRoot = Join-Path ([System.IO.Path]::GetTempPath()) ('adv-proj-' + [guid]::NewGuid().ToString('n'))
    New-Item -ItemType Directory -Path (Join-Path $tempRoot 'journal') -Force | Out-Null
    $filtered = @()
    $journalPath = Join-Path $featureRoot 'journal\events.jsonl'
    if (Test-Path -LiteralPath $journalPath) {
        foreach ($line in @(Get-Content -LiteralPath $journalPath)) {
            if ([string]::IsNullOrWhiteSpace($line)) { continue }
            try { $parsed = $line | ConvertFrom-Json } catch { continue }
            if ([string](Get-AdvisoryWorkflowProperty -Record $parsed -Name 'kind') -eq $script:AdvisoryWorkflowExcludedEventKind) { continue }
            $filtered += $line
        }
    }
    $filteredText = ''
    if (@($filtered).Count -gt 0) { $filteredText = ($filtered -join "`n") + "`n" }
    [System.IO.File]::WriteAllText((Join-Path $tempRoot 'journal\events.jsonl'), $filteredText, (New-Object System.Text.UTF8Encoding $false))
    try {
        Update-HarnessGateProjectionFromJournal -FeatureRoot $tempRoot -Feature $feature -ProfileName $profileName -RepoRoot $repoRoot -Write | Out-Null
        $before = Get-AdvisoryWorkflowProjectionFingerprint -FeatureRoot $tempRoot
        Update-HarnessGateProjectionFromJournal -FeatureRoot $featureRoot -Feature $feature -ProfileName $profileName -RepoRoot $repoRoot -Write | Out-Null
        $after = Get-AdvisoryWorkflowProjectionFingerprint -FeatureRoot $featureRoot
    } finally {
        Remove-Item -LiteralPath $tempRoot -Recurse -Force -ErrorAction SilentlyContinue
    }
    $hasAdvisory = Test-AdvisoryWorkflowReviewLoopHasAdvisoryState -FeatureRoot $featureRoot -ReportIds $reportIds
    return New-AdvisoryWorkflowResult -Fields @{
        status                                  = 'excluded'
        action                                  = 'projection-exclusion'
        action_id                               = [string]$Request.ActionId
        excluded_event_kinds                    = @($script:AdvisoryWorkflowExcludedEventKind)
        excluded_event_count                    = @($advisory).Count
        projection_exclusion_disposition        = 'excluded-no-op'
        mandatory_projection_fingerprint_before = $before
        mandatory_projection_fingerprint_after  = $after
        review_loop_has_advisory_state          = $hasAdvisory
        gate_effect                             = 'none'
    }
}

function Invoke-AdvisoryWorkflowVerify {
    param($Request)
    $featureRoot = [string]$Request.FeatureRoot
    $events = @(Get-AdvisoryWorkflowContinuityEvent -FeatureRoot $featureRoot)
    $eventRows = @()
    $reportId = $null
    $reportPath = $null
    foreach ($evt in @($events)) {
        $payload = Get-AdvisoryWorkflowProperty -Record $evt -Name 'payload'
        $eventRows += [pscustomobject]@{
            event_id = [string](Get-AdvisoryWorkflowProperty -Record $evt -Name 'event_id')
            action   = [string](Get-AdvisoryWorkflowProperty -Record $payload -Name 'action')
            report_id = [string](Get-AdvisoryWorkflowProperty -Record $payload -Name 'report_id')
        }
        if ($null -eq $reportId) {
            $reportId = [string](Get-AdvisoryWorkflowProperty -Record $payload -Name 'report_id')
            $reportPath = [string](Get-AdvisoryWorkflowProperty -Record $payload -Name 'report_path')
        }
    }
    $indexPath = Get-AdvisoryWorkflowIndexPath -FeatureRoot $featureRoot
    $index = $null
    if (Test-Path -LiteralPath $indexPath) {
        $index = Get-Content -LiteralPath $indexPath -Raw -Encoding UTF8 | ConvertFrom-Json
    }
    $loopPath = Join-Path $featureRoot '.review-loop.json'
    $ownershipBefore = $null
    if (Test-Path -LiteralPath $loopPath) {
        $beforeLoop = Get-Content -LiteralPath $loopPath -Raw -Encoding UTF8 | ConvertFrom-Json
        $ownershipBefore = [string](Get-AdvisoryWorkflowProperty -Record $beforeLoop -Name 'ownership_status')
    }
    $projection = Invoke-AdvisoryWorkflowProjectionExclusion -Request $Request
    $resumeRequest = $Request
    if (Test-AdvisoryWorkflowBlank ([string]$Request.MandatoryNextAction)) {
        $resumeRequest = [pscustomobject]@{}
        foreach ($property in @($Request.PSObject.Properties)) { $resumeRequest | Add-Member -NotePropertyName $property.Name -NotePropertyValue $property.Value }
        $resumeRequest | Add-Member -NotePropertyName MandatoryNextAction -NotePropertyValue 'canonical-mandatory-action' -Force
    }
    $resume = Invoke-AdvisoryWorkflowResume -Request $resumeRequest
    $ownershipAfter = $null
    if (Test-Path -LiteralPath $loopPath) {
        $afterLoop = Get-Content -LiteralPath $loopPath -Raw -Encoding UTF8 | ConvertFrom-Json
        $ownershipAfter = [string](Get-AdvisoryWorkflowProperty -Record $afterLoop -Name 'ownership_status')
    }
    return New-AdvisoryWorkflowResult -Fields @{
        status                      = 'verified'
        action                      = 'verify-integration'
        action_id                   = [string]$Request.ActionId
        report_id                   = $reportId
        report_path                 = $reportPath
        continuity_events           = @($eventRows)
        index_path                  = $(if ($null -eq $index) { $null } else { [string]$index.relative_path })
        index_schema_version        = $(if ($null -eq $index) { $null } else { [string]$index.schema_version })
        index_fingerprint           = $(if ($null -eq $index) { $null } else { [string]$index.index_fingerprint })
        can_satisfy_review_ready    = $(if ($null -eq $index) { $null } else { [bool]$index.can_satisfy_review_ready })
        resume                      = $resume
        projection_exclusion        = $projection
        mandatory_routing_changed   = $false
        review_ready_changed        = ($ownershipBefore -ne $ownershipAfter) -and ($ownershipAfter -eq 'review_ready')
        gate_effect                 = 'none'
    }
}

function Invoke-AdvisoryWorkflowAction {
    <#
    .SYNOPSIS
        Run one public advisory continuity action and return its result envelope.
    .DESCRIPTION
        Supports issue, decision, amendment, route, rebuild-index, resume-context,
        projection-exclusion, and verify-integration. Lifecycle success is recorded
        only when the report and one continuity event are both durable.
    #>

    [CmdletBinding()]
    param([Parameter(Mandatory = $true)]$Request)
    $action = [string](Get-AdvisoryWorkflowProperty -Record $Request -Name 'Action')
    $actionId = [string](Get-AdvisoryWorkflowProperty -Record $Request -Name 'ActionId')
    $feature = [string](Get-AdvisoryWorkflowProperty -Record $Request -Name 'Feature')
    if ($actionId -cnotmatch $script:AdvisoryWorkflowActionIdPattern) {
        return New-AdvisoryWorkflowFailure -Status 'failed' -Code 'INVALID_ACTION_ID' -Action $action -ActionId $actionId -Message 'action_id must be advact-<UUID>.'
    }
    if (Test-AdvisoryWorkflowBlank $feature) {
        return New-AdvisoryWorkflowFailure -Status 'failed' -Code 'MISSING_INPUT' -Action $action -ActionId $actionId -Message 'feature is required.'
    }
    if (Test-AdvisoryWorkflowBlank ([string](Get-AdvisoryWorkflowProperty -Record $Request -Name 'Actor'))) {
        return New-AdvisoryWorkflowFailure -Status 'failed' -Code 'MISSING_INPUT' -Action $action -ActionId $actionId -Message 'actor is required.'
    }
    try {
        switch ($action) {
            'issue' {
                $findings = ConvertFrom-AdvisoryWorkflowJsonArray -Text ([string]$Request.FindingsJson)
                $decisions = ConvertFrom-AdvisoryWorkflowJsonArray -Text ([string]$Request.DecisionsJson)
                $fingerprint = Get-AdvisoryWorkflowIssueFingerprint -Request $Request -Findings $findings -Decisions $decisions
                return Invoke-AdvisoryWorkflowIssue -Request $Request -Fingerprint $fingerprint
            }
            'decision' {
                $fingerprint = Get-AdvisoryWorkflowMutationFingerprint -Request $Request -ActionName 'decision'
                return Invoke-AdvisoryWorkflowDecision -Request $Request -Fingerprint $fingerprint
            }
            'amendment' {
                $fingerprint = Get-AdvisoryWorkflowMutationFingerprint -Request $Request -ActionName 'amendment'
                return Invoke-AdvisoryWorkflowAmendment -Request $Request -Fingerprint $fingerprint
            }
            'route' {
                $fingerprint = Get-AdvisoryWorkflowMutationFingerprint -Request $Request -ActionName 'route'
                return Invoke-AdvisoryWorkflowRoute -Request $Request -Fingerprint $fingerprint
            }
            'rebuild-index' { return Invoke-AdvisoryWorkflowRebuild -Request $Request }
            'resume-context' { return Invoke-AdvisoryWorkflowResume -Request $Request }
            'projection-exclusion' { return Invoke-AdvisoryWorkflowProjectionExclusion -Request $Request }
            'verify-integration' { return Invoke-AdvisoryWorkflowVerify -Request $Request }
            default {
                return New-AdvisoryWorkflowFailure -Status 'failed' -Code 'UNKNOWN_ACTION' -Action $action -ActionId $actionId -Message "Action '$action' is not a public advisory action."
            }
        }
    } catch {
        return New-AdvisoryWorkflowFailure -Status 'failed' -Code 'ACTION_FAILED' -Action $action -ActionId $actionId -Message $_.Exception.Message
    }
}

Export-ModuleMember -Function @('Invoke-AdvisoryWorkflowAction', 'Get-AdvisoryCrossEntrySentence', 'Get-AdvisoryActionClass')