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') |