Private/Invoke-ProposalApply.ps1

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

function Invoke-ProposalApply {
    <#
    .SYNOPSIS
        Applies a single taxonomy proposal (NEW/SPLIT/MERGE/RELABEL) to the taxonomy files.
    .DESCRIPTION
        Internal helper called by Approve-TaxonomyProposal. Mutates the taxonomy
        JSON file on disk and returns a result object.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [PSObject]$Proposal,

        [string]$RepoRoot = $script:RepoRoot
    )

    Set-StrictMode -Version Latest

    $TaxDir = Get-TaxonomyDir

    $PovFileMap = @{
        accelerationist = 'accelerationist.json'
        safetyist       = 'safetyist.json'
        skeptic         = 'skeptic.json'
        'cross-cutting' = 'cross-cutting.json'
    }

    $FileName = $PovFileMap[$Proposal.pov]
    if (-not $FileName) {
        return [PSCustomObject]@{ Success = $false; Error = "Unknown POV: $($Proposal.pov)" }
    }

    $FilePath = Join-Path $TaxDir $FileName
    if (-not (Test-Path $FilePath)) {
        return [PSCustomObject]@{ Success = $false; Error = "Taxonomy file not found: $FileName" }
    }

    try {
        $Raw = Get-Content -Raw -Path $FilePath | ConvertFrom-Json
    } catch {
        return [PSCustomObject]@{ Success = $false; Error = "Failed to parse $FileName`: $_" }
    }

    $IsCrossCutting = $Proposal.pov -eq 'cross-cutting'
    $Today = (Get-Date).ToString('yyyy-MM-dd')

    switch ($Proposal.action) {
        'NEW' {
            # Check for ID collision
            $Existing = $Raw.nodes | Where-Object { $_.id -eq $Proposal.suggested_id }
            if ($Existing) {
                return [PSCustomObject]@{ Success = $false; Error = "Node ID '$($Proposal.suggested_id)' already exists" }
            }

            if ($IsCrossCutting) {
                $NewNode = [ordered]@{
                    id              = $Proposal.suggested_id
                    label           = $Proposal.label
                    description     = $Proposal.description
                    interpretations = [ordered]@{
                        accelerationist = ''
                        safetyist       = ''
                        skeptic         = ''
                    }
                    linked_nodes    = @()
                    conflict_ids    = @()
                }
            } else {
                $NewNode = [ordered]@{
                    id                 = $Proposal.suggested_id
                    category           = $Proposal.category
                    label              = $Proposal.label
                    description        = $Proposal.description
                    parent_id          = $null
                    children           = @()
                    cross_cutting_refs = @()
                }
            }

            $Raw.nodes += $NewNode
        }

        'RELABEL' {
            $Target = $Raw.nodes | Where-Object { $_.id -eq $Proposal.target_node_id }
            if (-not $Target) {
                return [PSCustomObject]@{ Success = $false; Error = "Target node '$($Proposal.target_node_id)' not found" }
            }

            if ($Proposal.label) { $Target.label = $Proposal.label }
            if ($Proposal.description) { $Target.description = $Proposal.description }
        }

        'MERGE' {
            $SurvivorId = $Proposal.surviving_node_id
            $MergeIds   = @($Proposal.merge_node_ids)

            $Survivor = $Raw.nodes | Where-Object { $_.id -eq $SurvivorId }
            if (-not $Survivor) {
                return [PSCustomObject]@{ Success = $false; Error = "Surviving node '$SurvivorId' not found" }
            }

            # Update survivor label/description if proposal provides them
            if ($Proposal.label) { $Survivor.label = $Proposal.label }
            if ($Proposal.description) { $Survivor.description = $Proposal.description }

            # Remove merged nodes (except survivor)
            $RemoveIds = $MergeIds | Where-Object { $_ -ne $SurvivorId }
            $Raw.nodes = @($Raw.nodes | Where-Object { $_.id -notin $RemoveIds })

            # Update references in remaining nodes
            foreach ($Node in $Raw.nodes) {
                if ($Node.PSObject.Properties['children'] -and $Node.children) {
                    $Node.children = @($Node.children | ForEach-Object {
                        if ($_ -in $RemoveIds) { $SurvivorId } else { $_ }
                    } | Select-Object -Unique)
                }
                if ($Node.PSObject.Properties['cross_cutting_refs'] -and $Node.cross_cutting_refs) {
                    $Node.cross_cutting_refs = @($Node.cross_cutting_refs | ForEach-Object {
                        if ($_ -in $RemoveIds) { $SurvivorId } else { $_ }
                    } | Select-Object -Unique)
                }
                if ($Node.PSObject.Properties['parent_id'] -and $Node.parent_id -in $RemoveIds) {
                    $Node.parent_id = $SurvivorId
                }
            }

            Write-Warning "Merged nodes removed: $($RemoveIds -join ', '). Summaries and edges referencing these IDs may need updating."
        }

        'SPLIT' {
            $TargetId = $Proposal.target_node_id
            $Target = $Raw.nodes | Where-Object { $_.id -eq $TargetId }
            if (-not $Target) {
                return [PSCustomObject]@{ Success = $false; Error = "Target node '$TargetId' not found for SPLIT" }
            }

            $ChildProposals = @($Proposal.children)
            if ($ChildProposals.Count -eq 0) {
                return [PSCustomObject]@{ Success = $false; Error = "SPLIT proposal has no children" }
            }

            # Create child nodes
            foreach ($Child in $ChildProposals) {
                $ChildNode = [ordered]@{
                    id                 = $Child.suggested_id
                    category           = if ($Child.PSObject.Properties['category']) { $Child.category } else { $Target.category }
                    label              = $Child.label
                    description        = $Child.description
                    parent_id          = $TargetId
                    children           = @()
                    cross_cutting_refs = if ($Target.PSObject.Properties['cross_cutting_refs']) { $Target.cross_cutting_refs } else { @() }
                }
                $Raw.nodes += $ChildNode
            }

            # Update parent to reference children
            $Target.children = @($ChildProposals | ForEach-Object { $_.suggested_id })

            Write-Warning "Split '$TargetId' into $($ChildProposals.Count) children. Summaries referencing '$TargetId' may need re-processing."
        }

        default {
            return [PSCustomObject]@{ Success = $false; Error = "Unknown action: $($Proposal.action)" }
        }
    }

    $Raw.last_modified = $Today
    $Json = $Raw | ConvertTo-Json -Depth 20
    try {
        Set-Content -Path $FilePath -Value $Json -Encoding UTF8
    }
    catch {
        return [PSCustomObject]@{ Success = $false; Error = "Failed to write $FileName — $($_.Exception.Message)" }
    }

    return [PSCustomObject]@{ Success = $true; Error = $null }
}