Payload/scripts/kaden/advisory/AdvisoryIndexRebuild.ps1
|
function Get-AdvisoryWorkflowInventoryFingerprint { param($Files) $lines = @() foreach ($file in @($Files | Sort-Object { $_.relative_path })) { $lines += ($file.relative_path + '|' + $file.hash) } return Get-AdvisoryWorkflowSha256 -Text (($lines -join "`n")) } function Get-AdvisoryWorkflowNormalizedDiagnostic { param($Diagnostic) $code = [string](Get-AdvisoryWorkflowProperty -Record $Diagnostic -Name 'code') $reason = [string](Get-AdvisoryWorkflowProperty -Record $Diagnostic -Name 'reason') if (Test-AdvisoryWorkflowBlank $code) { if ($reason -match 'duplicate') { $code = 'duplicate-iteration' } elseif ($reason -match 'malformed') { $code = 'malformed' } else { $code = 'partial' } } $problem = [string](Get-AdvisoryWorkflowProperty -Record $Diagnostic -Name 'observed_problem') if (Test-AdvisoryWorkflowBlank $problem) { $problem = $reason } return New-AdvisoryWorkflowDiagnostic -Path ([string](Get-AdvisoryWorkflowProperty -Record $Diagnostic -Name 'path')) ` -Code $code -ObservedProblem $problem -Provenance ([string](Get-AdvisoryWorkflowProperty -Record $Diagnostic -Name 'provenance')) } function New-AdvisoryWorkflowPersistedIndex { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Returns an in-memory advisory index. It does not change system state.')] param( [Parameter(Mandatory = $true)][string]$Feature, [object[]]$Events, [object[]]$Reports, [object[]]$Diagnostics, [string]$JournalFingerprint, [string]$InventoryFingerprint ) $index = New-AdvisoryIndex -JournalEvents $Events -Reports $Reports $normalized = @() foreach ($diagnostic in @($Diagnostics)) { $normalized += Get-AdvisoryWorkflowNormalizedDiagnostic -Diagnostic $diagnostic } foreach ($diagnostic in @($index.diagnostics)) { $normalized += Get-AdvisoryWorkflowNormalizedDiagnostic -Diagnostic $diagnostic } $missing = @() $reportIds = @{} foreach ($report in @($Reports)) { $reportIds[[string](Get-AdvisoryWorkflowProperty -Record $report -Name 'report_id')] = $true } foreach ($evt in @($Events)) { if ([string](Get-AdvisoryWorkflowProperty -Record $evt -Name 'kind') -ne $script:AdvisoryWorkflowExcludedEventKind) { continue } $payload = Get-AdvisoryWorkflowProperty -Record $evt -Name 'payload' $reportId = [string](Get-AdvisoryWorkflowProperty -Record $payload -Name 'report_id') $reportPath = [string](Get-AdvisoryWorkflowProperty -Record $payload -Name 'report_path') if ((-not (Test-AdvisoryWorkflowBlank $reportId)) -and (-not $reportIds.ContainsKey($reportId))) { $missing += New-AdvisoryWorkflowDiagnostic -Path $reportPath -Code 'missing-reference' -ObservedProblem 'referenced report is missing' -Provenance 'journal' } } $normalized += @($missing) $body = [pscustomobject]@{ schema_version = 'advisory-index.v3' relative_path = (Get-AdvisoryWorkflowRepoRelativeIndexPath -Feature $Feature) feature = $Feature generated_from = 'journal + advisory report scan' latest_report_id = $index.latest_report_id latest_report_path = $index.latest_report_path reports = @($index.reports) source_event_ids = @($index.source_event_ids) timeline = @($index.timeline) diagnostics = @($normalized) can_satisfy_review_ready = $false gate_projection = 'excluded' source_journal_fingerprint = $JournalFingerprint report_inventory_fingerprint = $InventoryFingerprint } $indexFingerprint = Get-AdvisoryWorkflowSha256 -Text (ConvertTo-AdvisoryWorkflowCanonicalJson -Value $body) $body | Add-Member -NotePropertyName index_fingerprint -NotePropertyValue $indexFingerprint return $body } function Get-AdvisoryWorkflowIndexSnapshotLine { param([Parameter(Mandatory = $true)][string]$FeatureRoot) $path = Get-AdvisoryWorkflowIndexPath -FeatureRoot $FeatureRoot if (-not (Test-Path -LiteralPath $path)) { return @() } $lines = @() foreach ($line in @(Get-Content -LiteralPath $path)) { if ([string]::IsNullOrWhiteSpace($line)) { continue } $lines += $line } return @($lines) } function Get-AdvisoryWorkflowLastValidIndexSnapshot { param( [Parameter(Mandatory = $true)][string]$FeatureRoot, [string]$Feature = '' ) $lines = @((Get-AdvisoryWorkflowIndexSnapshotLine -FeatureRoot $FeatureRoot) | Select-Object -Last 50) for ($i = $lines.Count - 1; $i -ge 0; $i--) { $line = $lines[$i] try { $index = $line | ConvertFrom-Json } catch { continue } if ([string](Get-AdvisoryWorkflowProperty -Record $index -Name 'schema_version') -ne 'advisory-index.v3') { continue } if (-not (Test-AdvisoryWorkflowBlank $Feature)) { if ([string](Get-AdvisoryWorkflowProperty -Record $index -Name 'feature') -ne $Feature) { continue } } return $index } return $null } function Save-AdvisoryWorkflowIndex { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Appends one advisory index snapshot line after the candidate validates.')] param( [Parameter(Mandatory = $true)][string]$FeatureRoot, [Parameter(Mandatory = $true)]$Index, [switch]$ForceAppend ) $path = Get-AdvisoryWorkflowIndexPath -FeatureRoot $FeatureRoot $json = (ConvertTo-AdvisoryWorkflowCanonicalJson -Value $Index) $parent = Split-Path -Parent $path if (-not (Test-Path -LiteralPath $parent)) { New-Item -ItemType Directory -Path $parent -Force | Out-Null } $utf8 = New-Object System.Text.UTF8Encoding $false if (-not $ForceAppend) { $last = Get-AdvisoryWorkflowLastValidIndexSnapshot -FeatureRoot $FeatureRoot -Feature ([string](Get-AdvisoryWorkflowProperty -Record $Index -Name 'feature')) if ($null -ne $last) { $lastFingerprint = [string](Get-AdvisoryWorkflowProperty -Record $last -Name 'index_fingerprint') $nextFingerprint = [string](Get-AdvisoryWorkflowProperty -Record $Index -Name 'index_fingerprint') if ($lastFingerprint -eq $nextFingerprint) { return [pscustomobject]@{ path = $path; appended = $false; index = $last } } } } $existing = '' if (Test-Path -LiteralPath $path) { $existing = [System.IO.File]::ReadAllText($path, $utf8) if (($existing.Length -gt 0) -and (-not $existing.EndsWith("`n"))) { $existing += "`n" } } $candidate = $existing + $json + "`n" $temp = $path + '.tmp' [System.IO.File]::WriteAllText($temp, $candidate, $utf8) try { $null = ($json | ConvertFrom-Json) } catch { Remove-Item -LiteralPath $temp -Force -ErrorAction SilentlyContinue throw } if (Test-Path -LiteralPath $path) { [System.IO.File]::Replace($temp, $path, [NullString]::Value, $true) } else { [System.IO.File]::Move($temp, $path) } return [pscustomobject]@{ path = $path; appended = $true; index = $Index } } function Test-AdvisoryWorkflowIndexValid { param( [Parameter(Mandatory = $true)][string]$FeatureRoot, [Parameter(Mandatory = $true)][string]$Feature ) $reasons = @() $path = Get-AdvisoryWorkflowIndexPath -FeatureRoot $FeatureRoot if (-not (Test-Path -LiteralPath $path)) { return [pscustomobject]@{ valid = $false; reasons = @('missing'); index = $null } } $index = Get-AdvisoryWorkflowLastValidIndexSnapshot -FeatureRoot $FeatureRoot -Feature $Feature if ($null -eq $index) { return [pscustomobject]@{ valid = $false; reasons = @('unreadable'); index = $null } } if ([string](Get-AdvisoryWorkflowProperty -Record $index -Name 'schema_version') -ne 'advisory-index.v3') { $reasons += 'schema' } if ([string](Get-AdvisoryWorkflowProperty -Record $index -Name 'feature') -ne $Feature) { $reasons += 'feature' } $expectedPath = Get-AdvisoryWorkflowRepoRelativeIndexPath -Feature $Feature if ([string](Get-AdvisoryWorkflowProperty -Record $index -Name 'relative_path') -ne $expectedPath) { $reasons += 'path' } foreach ($required in @('generated_from', 'reports', 'source_event_ids', 'timeline', 'diagnostics', 'source_journal_fingerprint', 'report_inventory_fingerprint', 'index_fingerprint')) { if ($null -eq $index.PSObject.Properties[$required]) { $reasons += $required } } if ([bool](Get-AdvisoryWorkflowProperty -Record $index -Name 'can_satisfy_review_ready')) { $reasons += 'can_satisfy_review_ready' } if ([string](Get-AdvisoryWorkflowProperty -Record $index -Name 'gate_projection') -ne 'excluded') { $reasons += 'gate_projection' } $journalFingerprint = Get-AdvisoryWorkflowJournalFingerprint -FeatureRoot $FeatureRoot if ([string](Get-AdvisoryWorkflowProperty -Record $index -Name 'source_journal_fingerprint') -ne $journalFingerprint) { $reasons += 'source_journal_fingerprint' } $scan = Read-AdvisoryWorkflowScan -FeatureRoot $FeatureRoot -Feature $Feature if ($scan.failed) { $reasons += 'unreadable-report' } else { $inventory = Get-AdvisoryWorkflowInventoryFingerprint -Files $scan.files if ([string](Get-AdvisoryWorkflowProperty -Record $index -Name 'report_inventory_fingerprint') -ne $inventory) { $reasons += 'report_inventory_fingerprint' } $eventIds = @{} foreach ($evt in @(Get-AdvisoryWorkflowJournalEvent -FeatureRoot $FeatureRoot)) { $eventIds[[string](Get-AdvisoryWorkflowProperty -Record $evt -Name 'event_id')] = $true } foreach ($eventId in @($index.source_event_ids)) { if (-not $eventIds.ContainsKey([string]$eventId)) { $reasons += 'missing-event' } } foreach ($row in @($index.timeline)) { $rowId = [string](Get-AdvisoryWorkflowProperty -Record $row -Name 'event_id') if ((-not (Test-AdvisoryWorkflowBlank $rowId)) -and (-not $eventIds.ContainsKey($rowId))) { $reasons += 'missing-event' } } foreach ($report in @($index.reports)) { $reportPath = [string](Get-AdvisoryWorkflowProperty -Record $report -Name 'relative_path') $fileName = Split-Path -Leaf ($reportPath -replace '/', '\') $full = Join-Path (Get-AdvisoryWorkflowAdvisoryDirectory -FeatureRoot $FeatureRoot) $fileName if (-not (Test-Path -LiteralPath $full)) { $reasons += 'missing-report' } } } $unique = @($reasons | Select-Object -Unique) return [pscustomobject]@{ valid = ($unique.Count -eq 0); reasons = @($unique); index = $index } } function Get-AdvisoryWorkflowLastValidFingerprint { param([Parameter(Mandatory = $true)][string]$FeatureRoot) $index = Get-AdvisoryWorkflowLastValidIndexSnapshot -FeatureRoot $FeatureRoot if ($null -eq $index) { return $null } return [string](Get-AdvisoryWorkflowProperty -Record $index -Name 'index_fingerprint') } function Invoke-AdvisoryWorkflowRebuildCore { param( [Parameter(Mandatory = $true)][string]$FeatureRoot, [Parameter(Mandatory = $true)][string]$Feature, [string]$ReportScope = '' ) $journalFingerprint = Get-AdvisoryWorkflowJournalFingerprint -FeatureRoot $FeatureRoot $events = @(Get-AdvisoryWorkflowJournalEvent -FeatureRoot $FeatureRoot) $scan = Read-AdvisoryWorkflowScan -FeatureRoot $FeatureRoot -Feature $Feature if ($scan.failed) { return [pscustomobject]@{ ok = $false diagnostic = $scan.diagnostic journal_fingerprint = $journalFingerprint } } $reports = @($scan.valid) if (-not (Test-AdvisoryWorkflowBlank $ReportScope)) { $reports = @($reports | Where-Object { [string](Get-AdvisoryWorkflowProperty -Record $_ -Name 'report_id') -eq $ReportScope }) } $inventoryFiles = @($scan.files) if (-not (Test-AdvisoryWorkflowBlank $ReportScope)) { $inventoryFiles = @($inventoryFiles | Where-Object { $_.relative_path -match [regex]::Escape($ReportScope) }) } $inventoryFingerprint = Get-AdvisoryWorkflowInventoryFingerprint -Files $inventoryFiles $index = New-AdvisoryWorkflowPersistedIndex -Feature $Feature -Events $events -Reports $reports ` -Diagnostics $scan.diagnostics -JournalFingerprint $journalFingerprint -InventoryFingerprint $inventoryFingerprint $saved = Save-AdvisoryWorkflowIndex -FeatureRoot $FeatureRoot -Index $index $status = if ($saved.appended) { 'rebuilt' } else { 'current' } return [pscustomobject]@{ ok = $true result = (New-AdvisoryWorkflowResult -Fields @{ status = $status action = 'rebuild-index' index_path = (Get-AdvisoryWorkflowRepoRelativeIndexPath -Feature $Feature) index_schema_version = 'advisory-index.v3' index_fingerprint = [string]$saved.index.index_fingerprint source_journal_fingerprint = $journalFingerprint diagnostic_count = @($saved.index.diagnostics).Count latest_report_id = $saved.index.latest_report_id gate_effect = 'none' full_path = $saved.path appended = [bool]$saved.appended }) index = $saved.index } } |