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