Public/Approve-TaxonomyProposal.ps1

# Copyright (c) 2026 Jeffrey Snover. All rights reserved.
# Licensed under the MIT License. See LICENSE file in the project root.

function Approve-TaxonomyProposal {
    <#
    .SYNOPSIS
        Interactively reviews and applies taxonomy improvement proposals.
    .DESCRIPTION
        Loads a proposal JSON file generated by Invoke-TaxonomyProposal and
        presents each proposal for approval. Approved proposals are applied
        directly to the taxonomy JSON files.
    .PARAMETER Path
        Path to the proposal JSON file.
    .PARAMETER Interactive
        Review all pending proposals one by one.
    .PARAMETER Index
        Approve or reject a specific proposal by zero-based index.
    .PARAMETER Approve
        Set the proposal status to approved and apply it (with -Index).
    .PARAMETER Reject
        Set the proposal status to rejected (with -Index).
    .PARAMETER DryRun
        Show what each proposal would do without writing changes.
    .EXAMPLE
        Approve-TaxonomyProposal -Path taxonomy/proposals/proposal-2026-03-14.json -Interactive
    .EXAMPLE
        Approve-TaxonomyProposal -Path taxonomy/proposals/proposal-2026-03-14.json -Index 0 -Approve
    .EXAMPLE
        Approve-TaxonomyProposal -Path taxonomy/proposals/proposal-2026-03-14.json -Index 2 -Reject
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory, Position = 0)]
        [string]$Path,

        [switch]$Interactive,

        [int]$Index = -1,

        [switch]$Approve,

        [switch]$Reject,

        [switch]$DryRun
    )

    Set-StrictMode -Version Latest

    if (-not (Test-Path $Path)) {
        Write-Fail "Proposal file not found: $Path"
        return
    }

    $ProposalData = Get-Content -Raw -Path $Path | ConvertFrom-Json

    if (-not $ProposalData.proposals -or $ProposalData.proposals.Count -eq 0) {
        Write-OK 'No proposals in file.'
        return
    }

    $ActionColors = @{
        'NEW'     = 'Green'
        'SPLIT'   = 'Cyan'
        'MERGE'   = 'Yellow'
        'RELABEL' = 'Magenta'
    }

    function Show-Proposal {
        param([int]$Idx, [PSObject]$P)

        $Color = $ActionColors[$P.action]
        if (-not $Color) { $Color = 'White' }

        Write-Host "[$Idx] " -NoNewline -ForegroundColor DarkGray
        Write-Host "$($P.action)" -NoNewline -ForegroundColor $Color
        Write-Host " $($P.suggested_id)" -ForegroundColor White

        Write-Host " POV: $($P.pov) | Category: $($P.category)" -ForegroundColor DarkGray
        Write-Host " Label: $($P.label)" -ForegroundColor White

        # Truncate description for display
        $Desc = if ($P.description.Length -gt 150) { $P.description.Substring(0, 150) + '...' } else { $P.description }
        Write-Host " Desc: $Desc" -ForegroundColor Gray

        if ($P.target_node_id) {
            Write-Host " Target: $($P.target_node_id)" -ForegroundColor DarkYellow
        }

        # Rationale
        $Rat = if ($P.rationale.Length -gt 200) { $P.rationale.Substring(0, 200) + '...' } else { $P.rationale }
        Write-Host " Reason: $Rat" -ForegroundColor Gray

        # Evidence
        if ($P.PSObject.Properties['evidence_doc_ids'] -and $P.evidence_doc_ids) {
            $DocList = ($P.evidence_doc_ids | Select-Object -First 5) -join ', '
            Write-Host " Evidence: $DocList ($($P.evidence_count) docs)" -ForegroundColor DarkGray
        }

        # Action-specific details
        if ($P.action -eq 'SPLIT' -and $P.PSObject.Properties['children'] -and $P.children) {
            Write-Host " Children:" -ForegroundColor Cyan
            foreach ($C in $P.children) {
                Write-Host " - $($C.suggested_id): $($C.label)" -ForegroundColor White
            }
        }
        if ($P.action -eq 'MERGE' -and $P.PSObject.Properties['merge_node_ids']) {
            Write-Host " Merge: $($P.merge_node_ids -join ' + ') -> $($P.surviving_node_id)" -ForegroundColor Yellow
        }

        # Status if already decided
        if ($P.PSObject.Properties['status'] -and $P.status -ne 'pending') {
            Write-Host " Status: $($P.status)" -ForegroundColor DarkGray
        }

        Write-Host ''
    }

    if ($Interactive) {
        # Collect pending proposals
        $Pending = @()
        for ($i = 0; $i -lt $ProposalData.proposals.Count; $i++) {
            $P = $ProposalData.proposals[$i]
            $HasStatus = $P.PSObject.Properties['status']
            if (-not $HasStatus -or $P.status -eq 'pending') {
                $Pending += [PSCustomObject]@{ Index = $i; Proposal = $P }
            }
        }

        if ($Pending.Count -eq 0) {
            Write-OK 'No pending proposals to review.'
            return
        }

        Write-Host ''
        Write-Host "=== Taxonomy Proposal Review: $($Pending.Count) pending ===" -ForegroundColor Cyan
        Write-Host ''

        $ApprovedCount = 0
        $RejectedCount = 0
        $SkippedCount  = 0

        foreach ($Item in $Pending) {
            Show-Proposal -Idx $Item.Index -P $Item.Proposal

            $Choice = Read-Host ' (a)pprove / (r)eject / (s)kip / (q)uit'
            switch ($Choice.ToLower()) {
                'a' {
                    if ($DryRun) {
                        Write-Info "DRY RUN: Would apply $($Item.Proposal.action) for $($Item.Proposal.suggested_id)"
                        $ProposalData.proposals[$Item.Index] |
                            Add-Member -NotePropertyName 'status' -NotePropertyValue 'approved' -Force
                    } else {
                        $Result = Invoke-ProposalApply -Proposal $Item.Proposal
                        if ($Result.Success) {
                            Write-OK "Applied: $($Item.Proposal.action) $($Item.Proposal.suggested_id)"
                            $ProposalData.proposals[$Item.Index] |
                                Add-Member -NotePropertyName 'status' -NotePropertyValue 'approved' -Force
                        } else {
                            Write-Fail "Failed: $($Result.Error)"
                            $ProposalData.proposals[$Item.Index] |
                                Add-Member -NotePropertyName 'status' -NotePropertyValue 'failed' -Force
                        }
                    }
                    $ApprovedCount++
                }
                'r' {
                    $ProposalData.proposals[$Item.Index] |
                        Add-Member -NotePropertyName 'status' -NotePropertyValue 'rejected' -Force
                    $RejectedCount++
                    Write-Info 'Rejected'
                }
                'q' {
                    Write-Info 'Quitting review.'
                    break
                }
                default {
                    $SkippedCount++
                    Write-Info 'Skipped'
                }
            }
            Write-Host ''

            if ($Choice.ToLower() -eq 'q') { break }
        }

        Write-Host ''
        Write-Host "Review complete: $ApprovedCount approved, $RejectedCount rejected, $SkippedCount skipped" -ForegroundColor Cyan

    } elseif ($Index -ge 0) {
        if ($Index -ge $ProposalData.proposals.Count) {
            Write-Fail "Index $Index out of range (0-$($ProposalData.proposals.Count - 1))"
            return
        }

        if ($Approve -and $Reject) {
            Write-Fail 'Specify either -Approve or -Reject, not both.'
            return
        }
        if (-not $Approve -and -not $Reject) {
            Write-Fail 'Specify either -Approve or -Reject.'
            return
        }

        $P = $ProposalData.proposals[$Index]
        Show-Proposal -Idx $Index -P $P

        if ($Approve) {
            if ($DryRun) {
                Write-Info "DRY RUN: Would apply $($P.action) for $($P.suggested_id)"
                $P | Add-Member -NotePropertyName 'status' -NotePropertyValue 'approved' -Force
            } elseif ($PSCmdlet.ShouldProcess("Proposal $Index ($($P.action) $($P.suggested_id))", 'Apply')) {
                $Result = Invoke-ProposalApply -Proposal $P
                if ($Result.Success) {
                    Write-OK "Applied: $($P.action) $($P.suggested_id)"
                    $P | Add-Member -NotePropertyName 'status' -NotePropertyValue 'approved' -Force
                } else {
                    Write-Fail "Failed: $($Result.Error)"
                    $P | Add-Member -NotePropertyName 'status' -NotePropertyValue 'failed' -Force
                }
            }
        } else {
            $P | Add-Member -NotePropertyName 'status' -NotePropertyValue 'rejected' -Force
            Write-Info 'Rejected'
        }
    } else {
        Write-Fail 'Specify -Interactive, or -Index with -Approve/-Reject.'
        return
    }

    # Save updated proposal file with statuses
    if ($PSCmdlet.ShouldProcess($Path, 'Write updated proposal file')) {
        try {
            $Json = $ProposalData | ConvertTo-Json -Depth 20
            Set-Content -Path $Path -Value $Json -Encoding UTF8
            Write-OK "Saved $Path"
        }
        catch {
            Write-Fail "Failed to write proposal file — $($_.Exception.Message)"
            Write-Info "Proposal statuses were NOT saved. Check file permissions and try again."
        }
    }

    # Post-apply reminders
    Write-Host ''
    Write-Host ' Reminders:' -ForegroundColor DarkGray
    Write-Host ' - Run Update-TaxEmbeddings to regenerate embeddings' -ForegroundColor DarkGray
    Write-Host ' - Run Invoke-BatchSummary -ForceAll if nodes were split/merged' -ForegroundColor DarkGray
    Write-Host ' - Run Invoke-EdgeDiscovery for new/changed nodes' -ForegroundColor DarkGray
}