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
    }
}