Public/Get-ConflictEvolution.ps1

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

function Get-ConflictEvolution {
    <#
    .SYNOPSIS
        Analyzes how conflicts evolve across sources using graph-aware reasoning.
    .DESCRIPTION
        For a given conflict (or all conflicts), loads the linked taxonomy nodes
        and their graph edges, then uses an LLM to analyze:
        - Whether positions are converging or diverging
        - Which assumptions underlie each side
        - What evidence would resolve the conflict
        - Which graph paths connect the conflicting claims

        Without -Analyze, returns a structured summary of each conflict's
        graph context (linked nodes, edges between them, source instances).
        With -Analyze, sends the graph context to an LLM for deeper reasoning.
    .PARAMETER Id
        Conflict ID to analyze (e.g., "conflict-agi-timelines-001").
        If omitted, processes all conflicts.
    .PARAMETER Analyze
        Use LLM to generate a deep analysis of conflict evolution.
        Without this switch, only structured graph context is returned.
    .PARAMETER Model
        AI model to use for analysis. Defaults to 'gemini-2.5-flash'.
    .PARAMETER ApiKey
        AI API key.
    .PARAMETER RepoRoot
        Path to the repository root.
    .EXAMPLE
        Get-ConflictEvolution
    .EXAMPLE
        Get-ConflictEvolution -Id "conflict-agi-timelines-001"
    .EXAMPLE
        Get-ConflictEvolution -Id "conflict-agi-timelines-001" -Analyze
    .EXAMPLE
        Get-ConflictEvolution -Analyze | Where-Object { $_.analysis.convergence_trend -eq 'diverging' }
    #>

    [CmdletBinding()]
    param(
        [string]$Id = '',

        [switch]$Analyze,

        [ValidateScript({ Test-AIModelId $_ })]
        [ArgumentCompleter({ param($cmd, $param, $word) $script:ValidModelIds | Where-Object { $_ -like "$word*" } })]
        [string]$Model = 'gemini-2.5-flash',

        [string]$ApiKey = '',

        [string]$RepoRoot = $script:RepoRoot
    )

    Set-StrictMode -Version Latest
    $ErrorActionPreference = 'Stop'

    # ── Load taxonomy nodes ──
    $TaxDir = Get-TaxonomyDir
    $AllNodes = @{}
    $NodePovMap = @{}
    foreach ($PovKey in @('accelerationist', 'safetyist', 'skeptic', 'cross-cutting')) {
        $FilePath = Join-Path $TaxDir "$PovKey.json"
        if (-not (Test-Path $FilePath)) { continue }
        $FileData = Get-Content -Raw -Path $FilePath | ConvertFrom-Json
        foreach ($Node in $FileData.nodes) {
            $AllNodes[$Node.id] = $Node
            $NodePovMap[$Node.id] = $PovKey
        }
    }

    # ── Load edges ──
    $EdgesPath = Join-Path $TaxDir 'edges.json'
    $AllEdges = @()
    if (Test-Path $EdgesPath) {
        $EdgesData = Get-Content -Raw -Path $EdgesPath | ConvertFrom-Json
        $AllEdges = @($EdgesData.edges)
    }

    # ── Load conflicts ──
    $ConflictDir = Get-ConflictsDir
    if (-not (Test-Path $ConflictDir)) {
        Write-Fail "Conflicts directory not found: $ConflictDir"
        return
    }

    $ConflictFiles = Get-ChildItem -Path $ConflictDir -Filter '*.json' -File
    $Conflicts = [System.Collections.Generic.List[PSObject]]::new()

    foreach ($File in $ConflictFiles) {
        try {
            $Conflict = Get-Content -Raw -Path $File.FullName | ConvertFrom-Json
            if ($Id -and $Conflict.claim_id -ne $Id) { continue }
            $Conflicts.Add($Conflict)
        } catch {
            Write-Warn "Failed to load $($File.Name): $_"
        }
    }

    if ($Conflicts.Count -eq 0) {
        if ($Id) {
            Write-Fail "Conflict not found: $Id"
        } else {
            Write-Warn 'No conflicts found.'
        }
        return
    }

    Write-Step "Analyzing $($Conflicts.Count) conflict(s)"

    # ── Resolve API key if analyzing ──
    $ResolvedKey = $null
    if ($Analyze) {
        $Backend = if     ($Model -match '^gemini') { 'gemini' }
                   elseif ($Model -match '^claude') { 'claude' }
                   elseif ($Model -match '^groq')   { 'groq'   }
                   else                             { 'gemini'  }
        $ResolvedKey = Resolve-AIApiKey -ExplicitKey $ApiKey -Backend $Backend
        if ([string]::IsNullOrWhiteSpace($ResolvedKey)) {
            Write-Fail 'No API key found. Set GEMINI_API_KEY, ANTHROPIC_API_KEY, or AI_API_KEY.'
            throw 'No API key configured'
        }
    }

    # ── Process each conflict ──
    $Results = [System.Collections.Generic.List[PSObject]]::new()

    foreach ($Conflict in $Conflicts) {
        Write-Info "$($Conflict.claim_id): $($Conflict.claim_label)"

        # Find linked nodes
        $LinkedNodeIds = @()
        if ($Conflict.PSObject.Properties['linked_taxonomy_nodes']) {
            $LinkedNodeIds = @($Conflict.linked_taxonomy_nodes)
        }

        $LinkedNodes = foreach ($NId in $LinkedNodeIds) {
            if ($AllNodes.ContainsKey($NId)) {
                $N = $AllNodes[$NId]
                [ordered]@{
                    id               = $N.id
                    pov              = $NodePovMap[$NId]
                    label            = $N.label
                    description      = $N.description
                    graph_attributes = if ($N.PSObject.Properties['graph_attributes']) { $N.graph_attributes } else { $null }
                }
            }
        }

        # Find edges between linked nodes (and edges involving linked nodes)
        $LinkedNodeSet = [System.Collections.Generic.HashSet[string]]::new()
        foreach ($NId in $LinkedNodeIds) { [void]$LinkedNodeSet.Add($NId) }

        $RelevantEdges = foreach ($Edge in $AllEdges) {
            $SourceLinked = $LinkedNodeSet.Contains($Edge.source)
            $TargetLinked = $LinkedNodeSet.Contains($Edge.target)
            if ($SourceLinked -or $TargetLinked) {
                [ordered]@{
                    source     = $Edge.source
                    target     = $Edge.target
                    type       = $Edge.type
                    confidence = $Edge.confidence
                    status     = $Edge.status
                    rationale  = if ($Edge.PSObject.Properties['rationale']) { $Edge.rationale } else { '' }
                    both_linked = ($SourceLinked -and $TargetLinked)
                }
            }
        }

        # Count instances by stance
        $Instances = @($Conflict.instances | Where-Object { $_.doc_id -ne '_seed' })
        # Support both new schema (stance field) and legacy (position string)
        $SupportsCount = @($Instances | Where-Object {
            ($_.PSObject.Properties['stance'] -and $_.stance -eq 'supports') -or
            (-not $_.PSObject.Properties['stance'] -and $_.position -match '^supports')
        }).Count
        $DisputesCount = @($Instances | Where-Object {
            ($_.PSObject.Properties['stance'] -and $_.stance -eq 'disputes') -or
            (-not $_.PSObject.Properties['stance'] -and $_.position -match '^disputes')
        }).Count
        $QualifiesCount = @($Instances | Where-Object {
            $_.PSObject.Properties['stance'] -and $_.stance -eq 'qualifies'
        }).Count
        $NeutralCount  = $Instances.Count - $SupportsCount - $DisputesCount - $QualifiesCount

        $ConflictContext = [PSCustomObject][ordered]@{
            conflict_id      = $Conflict.claim_id
            claim_label      = $Conflict.claim_label
            description      = $Conflict.description
            status           = $Conflict.status
            linked_nodes     = @($LinkedNodes)
            edges            = @($RelevantEdges)
            internal_edges   = @($RelevantEdges | Where-Object { $_.both_linked }).Count
            instance_count   = $Instances.Count
            supports_count   = $SupportsCount
            disputes_count   = $DisputesCount
            qualifies_count  = $QualifiesCount
            neutral_count    = $NeutralCount
            instances        = $Instances
        }

        if (-not $Analyze) {
            $Results.Add($ConflictContext)
            continue
        }

        # ── LLM Analysis ──
        $ConflictJson = $ConflictContext | ConvertTo-Json -Depth 10

        $AnalysisPrompt = @"
You are analyzing the evolution and structure of a factual conflict in the AI policy debate.

A conflict represents a disputed claim where different sources and POVs disagree. You are given:
1. The conflict description and linked taxonomy nodes.
2. Graph edges involving those nodes (with types, confidence, and rationale).
3. Source instances — each recording a document's position on the claim.

Analyze this conflict and respond in JSON:

{
  "convergence_trend": "converging|diverging|stable|insufficient_data",
  "convergence_reasoning": "Why you assessed the trend this way",
  "key_assumptions": [
    {
      "assumption": "The assumption text",
      "held_by": ["pov1", "pov2"],
      "contested_by": ["pov3"],
      "graph_evidence": "Which edges/nodes support this"
    }
  ],
  "resolution_paths": [
    {
      "description": "What evidence or concession could resolve this",
      "type": "empirical_evidence|conceptual_reframing|assumption_challenge|scope_narrowing",
      "feasibility": "high|medium|low"
    }
  ],
  "graph_insights": [
    "Observations about the graph structure around this conflict"
  ],
  "pov_positions": {
    "accelerationist": "Summary of accelerationist stance on this conflict",
    "safetyist": "Summary of safetyist stance",
    "skeptic": "Summary of skeptic stance"
  },
  "evidence_balance": {
    "empirical_support": "strong|moderate|weak|none",
    "source_diversity": "high|medium|low",
    "assessment": "Overall evidence quality assessment"
  }
}

--- CONFLICT DATA ---
$ConflictJson
"@


        try {
            $AIResult = Invoke-AIApi `
                -Prompt $AnalysisPrompt `
                -Model $Model `
                -ApiKey $ResolvedKey `
                -Temperature 0.3 `
                -MaxTokens 4096 `
                -JsonMode `
                -TimeoutSec 120

            $ResponseText = $AIResult.Text -replace '^\s*```json\s*', '' -replace '\s*```\s*$', ''
            try {
                $Analysis = $ResponseText | ConvertFrom-Json -Depth 20
            } catch {
                $Repaired = Repair-TruncatedJson -Text $ResponseText
                $Analysis = $Repaired | ConvertFrom-Json -Depth 20
            }

            $ConflictContext | Add-Member -NotePropertyName 'analysis' -NotePropertyValue $Analysis
            Write-OK "$($Conflict.claim_id): $($Analysis.convergence_trend)"
        } catch {
            Write-Fail "$($Conflict.claim_id): analysis failed — $_"
            $ConflictContext | Add-Member -NotePropertyName 'analysis' -NotePropertyValue $null
        }

        $Results.Add($ConflictContext)
    }

    # ── Output ──
    if ($Results.Count -eq 1) {
        return $Results[0]
    }
    return $Results
}