Private/Update-DebateProgress.ps1

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

# Atomic per-turn progress writer for debate runs.
# Dot-sourced by AITriad.psm1 — do NOT export.

function Update-DebateProgress {
    <#
    .SYNOPSIS
        Atomically updates a debate-progress.json status file.
    .DESCRIPTION
        Read-modify-write of a JSON status file shared between a debate runner
        and Watch-DebateProgress. Writes via a temp file + Move-Item so the
        watcher never reads a half-written document.
 
        Caller supplies the debate identity (BatchName + DebateName) and a
        hashtable of fields to merge onto that debate's entry. Initializes the
        file on first write. Always stamps last_update_at to now (UTC).
    .PARAMETER Path
        Absolute path to debate-progress.json.
    .PARAMETER BatchName
        Top-level batch identifier. Set on first write; subsequent writes leave
        it alone unless explicitly overridden.
    .PARAMETER DebateName
        Per-debate identifier. The function upserts the entry with this name.
    .PARAMETER Fields
        Hashtable of fields to merge onto the debate entry. Common keys:
        status, started_at, current_turn, total_turns_expected, current_stage,
        current_debater, error.
    .PARAMETER Debates
        Optional. List of debate names to seed the file with as 'pending' on
        first write. Used by Invoke-DebateBatch to publish the full work list.
    .EXAMPLE
        Update-DebateProgress -Path $p -BatchName 'b1' -DebateName 'd1' -Fields @{
            status = 'running'; current_turn = 4; current_stage = 'draft'
        }
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Path,

        [Parameter(Mandatory)]
        [string]$DebateName,

        [Parameter()]
        [string]$BatchName,

        [Parameter()]
        [hashtable]$Fields = @{},

        [Parameter()]
        [string[]]$Debates
    )

    Set-StrictMode -Version Latest

    $NowUtc = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')

    # ── Load or initialize ──────────────────────────────────
    $State = $null
    if (Test-Path $Path) {
        try {
            $Raw = Get-Content -Raw -Path $Path -ErrorAction Stop
            if (-not [string]::IsNullOrWhiteSpace($Raw)) {
                $State = $Raw | ConvertFrom-Json -AsHashtable
            }
        } catch {
            # Corrupted file (e.g. crash mid-write) — start fresh rather than fail the debate
            Write-Verbose "Update-DebateProgress: existing file unreadable, reinitializing ($($_.Exception.Message))"
        }
    }
    if (-not $State) {
        $State = @{
            batch_name = if ($BatchName) { $BatchName } else { '' }
            started_at = $NowUtc
            debates    = @()
        }
    }

    # Late-arriving BatchName overrides empty initial value
    if ($BatchName -and [string]::IsNullOrWhiteSpace($State['batch_name'])) {
        $State['batch_name'] = $BatchName
    }

    # Seed pending debates on first write
    if ($Debates -and @($State['debates']).Count -eq 0) {
        $Seeded = [System.Collections.Generic.List[hashtable]]::new()
        foreach ($Name in $Debates) {
            $Seeded.Add(@{ name = $Name; status = 'pending' })
        }
        $State['debates'] = $Seeded.ToArray()
    }

    # ── Upsert the named debate ─────────────────────────────
    $List = [System.Collections.Generic.List[hashtable]]::new()
    foreach ($D in @($State['debates'])) { $List.Add([hashtable]$D) }

    $Entry = $null
    foreach ($D in $List) {
        if ($D['name'] -eq $DebateName) { $Entry = $D; break }
    }
    if (-not $Entry) {
        $Entry = @{ name = $DebateName; status = 'pending' }
        $List.Add($Entry)
    }

    foreach ($Key in $Fields.Keys) {
        $Entry[$Key] = $Fields[$Key]
    }
    $Entry['last_update_at'] = $NowUtc

    $State['debates'] = $List.ToArray()

    # ── Atomic write ────────────────────────────────────────
    $Dir = Split-Path -Parent $Path
    if ($Dir -and -not (Test-Path $Dir)) {
        $null = New-Item -ItemType Directory -Path $Dir -Force
    }
    $TempPath = "$Path.tmp"
    $Json = $State | ConvertTo-Json -Depth 10
    Set-Content -Path $TempPath -Value $Json -Encoding utf8NoBOM -NoNewline
    # Move-Item -Force on Windows replaces atomically when source/dest are on the same volume
    Move-Item -Path $TempPath -Destination $Path -Force
}