Public/Update-NodeTestingRecord.ps1

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

function Update-NodeTestingRecord {
    <#
    .SYNOPSIS
        Recomputes tier + sort_key on every node's existing debate-tested
        record under current constant values, without ever touching
        historical fields (Phase 2 of t/1523; ticket t/1579).
    .DESCRIPTION
        Sweeps POV taxonomy files, applies the pure Get-DebateTestedTier
        function against each node's existing `record[]` + `revisions[]`,
        and rewrites ONLY `tier` and `sort_key` when they differ from
        what's currently stored. Everything else in the debate_tested
        block — `record`, `engagements`, `challenges`, `held`, `weakened`,
        `revisions`, `last_tested`, `description_hash` — is historical
        fact and is never rewritten.
 
        Purpose: when a debate-tested constant changes (e.g. raising
        WELL_TESTED_MIN_CHALLENGES 2 → 3), this cmdlet applies the change
        across all accumulated data in one pass, without re-running any
        debates. The operation is idempotent: run twice, second run
        reports zero changes.
 
        The pure recompute logic (Get-DebateTestedTier) is a faithful
        port of the TypeScript writer at lib/debate/debateTested.ts.
        AC #10 of t/1579 enforces cross-language consistency via
        tests/fixtures/tier-sort-key-cases.json — both suites consume
        the same fixture so any drift fails both sides.
 
    .PARAMETER Pov
        Restrict to a single camp (acc / saf / skp). Default: all.
    .PARAMETER NodeId
        Restrict to specific node id(s). Default: every node with a
        debate_tested record.
    .PARAMETER RecomputeOnly
        Required for phase-2 write mode — the cmdlet's only current
        operation. Present to reserve the parameter name for future
        modes (e.g. `-BackfillDescriptionHash`).
    .OUTPUTS
        [PSCustomObject] per changed node with NodeId, Pov, OldTier,
        NewTier, OldSortKey, NewSortKey. Emits nothing for nodes whose
        tier + sort_key were already up-to-date.
    .EXAMPLE
        Update-NodeTestingRecord -RecomputeOnly -WhatIf
    .EXAMPLE
        Update-NodeTestingRecord -RecomputeOnly -Pov saf
    .LINK
        Get-NodeTestingRecord
    .LINK
        Get-Tax
    .LINK
        Get-TaxonomyHealth
    #>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Low')]
    param(
        [Parameter()]
        [ValidateSet('acc', 'saf', 'skp')]
        [string]$Pov,

        [Parameter()]
        [string[]]$NodeId,

        [Parameter(Mandatory)]
        [switch]$RecomputeOnly
    )

    Set-StrictMode -Version Latest

    $povFullByShort = @{ acc = 'accelerationist'; saf = 'safetyist'; skp = 'skeptic' }
    $povFiles = if ($Pov) { @($povFullByShort[$Pov]) } else { @('accelerationist', 'safetyist', 'skeptic') }
    $wantedIds = if ($NodeId -and $NodeId.Count -gt 0) { [System.Collections.Generic.HashSet[string]]::new([string[]]$NodeId, [System.StringComparer]::OrdinalIgnoreCase) } else { $null }

    $taxDir = Get-TaxonomyDir
    $changes = [System.Collections.Generic.List[PSObject]]::new()
    $dirtyFiles = @{}

    foreach ($povName in $povFiles) {
        $filePath = Join-Path $taxDir "$povName.json"
        if (-not (Test-Path $filePath)) {
            Write-Warning "Update-NodeTestingRecord: POV file not found: $filePath — skipping"
            continue
        }
        $store = Get-Content $filePath -Raw | ConvertFrom-Json
        if (-not $store.PSObject.Properties['nodes']) {
            Write-Warning "Update-NodeTestingRecord: $filePath has no 'nodes' — skipping"
            continue
        }

        $mutated = $false
        foreach ($node in @($store.nodes)) {
            $nodeIdStr = if ($node.PSObject.Properties['id']) { [string]$node.id } else { $null }
            if (-not $nodeIdStr) { continue }
            if ($wantedIds -and -not $wantedIds.Contains($nodeIdStr)) { continue }

            if (-not $node.PSObject.Properties['graph_attributes']) { continue }
            $ga = $node.graph_attributes
            if ($null -eq $ga -or
                -not $ga.PSObject.Properties['debate_tested'] -or
                $null -eq $ga.debate_tested) { continue }
            $dt = $ga.debate_tested

            # Historical fields required for recompute
            $record    = if ($dt.PSObject.Properties['record'])    { @($dt.record) }    else { @() }
            $revisions = if ($dt.PSObject.Properties['revisions']) { @($dt.revisions) } else { @() }

            $r = Get-DebateTestedTier -Record $record -Revisions $revisions
            $newTier = [string]$r.tier
            $newSort = [double]$r.sort_key

            $oldTier = if ($dt.PSObject.Properties['tier'])     { [string]$dt.tier }        else { '' }
            $oldSort = if ($dt.PSObject.Properties['sort_key']) { [double]$dt.sort_key }    else { 0.0 }

            if ($oldTier -eq $newTier -and $oldSort -eq $newSort) { continue }

            $changes.Add([PSCustomObject]@{
                NodeId     = $nodeIdStr
                Pov        = $povName
                OldTier    = $oldTier
                NewTier    = $newTier
                OldSortKey = $oldSort
                NewSortKey = $newSort
            })

            if ($PSCmdlet.ShouldProcess("$povName/$nodeIdStr", "recompute tier ($oldTier→$newTier), sort_key ($oldSort→$newSort)")) {
                # In-place rewrite of ONLY tier + sort_key.
                if ($dt.PSObject.Properties['tier']) { $dt.tier = $newTier }
                else { Add-Member -InputObject $dt -MemberType NoteProperty -Name 'tier' -Value $newTier -Force }
                if ($dt.PSObject.Properties['sort_key']) { $dt.sort_key = $newSort }
                else { Add-Member -InputObject $dt -MemberType NoteProperty -Name 'sort_key' -Value $newSort -Force }
                $mutated = $true
            }
        }

        if ($mutated) { $dirtyFiles[$povName] = $store }
    }

    if ($dirtyFiles.Count -gt 0 -and -not $WhatIfPreference) {
        foreach ($povName in $dirtyFiles.Keys) {
            $filePath = Join-Path $taxDir "$povName.json"
            $dirtyFiles[$povName] | ConvertTo-Json -Depth 20 | Set-Content -Path $filePath -Encoding utf8NoBOM
            Write-Verbose "Update-NodeTestingRecord: wrote $filePath"
        }
    }

    Write-Host ''
    if ($WhatIfPreference) {
        Write-Host "Would recompute tier/sort_key on $($changes.Count) node(s). No changes written."
    } else {
        Write-Host "Recomputed tier/sort_key on $($changes.Count) node(s). Files updated: $($dirtyFiles.Count)."
    }

    $changes
}