Public/Repair-PovAttributes.ps1
|
# Copyright (c) 2026 Jeffrey Snover. All rights reserved. # Licensed under the MIT License. See LICENSE file in the project root. function Repair-PovAttributes { <# .SYNOPSIS Fills missing graph_attributes on POV taxonomy nodes using AI. .DESCRIPTION Scans POV nodes for missing graph_attributes fields and uses AI to generate them. Priority order: 1. Nodes with NO graph_attributes at all (all fields needed) 2. Missing steelman_vulnerability + intellectual_lineage 3. Missing possible_fallacies (lower priority, many legitimately have none) The steelman_vulnerability is generated as a per-opponent-POV object. .PARAMETER POV Filter to a specific POV file. .PARAMETER Category Filter to a specific BDI category. .PARAMETER Priority Which priority level to fix: 'all' (default), 'critical' (no GA + missing steelman/lineage), or 'full' (includes possible_fallacies). .PARAMETER Model AI model. Default: gemini-3.1-flash-lite-preview. .PARAMETER ApiKey AI API key. .PARAMETER BatchSize Nodes per AI call. Default: 10. .EXAMPLE Repair-PovAttributes -WhatIf .EXAMPLE Repair-PovAttributes -POV safetyist -Priority critical .EXAMPLE Repair-PovAttributes -Priority full #> [CmdletBinding(SupportsShouldProcess)] param( [ValidateSet('accelerationist', 'safetyist', 'skeptic')] [string]$POV, [ValidateSet('Beliefs', 'Desires', 'Intentions')] [string]$Category, [ValidateSet('all', 'critical', 'full')] [string]$Priority = 'all', [ValidateScript({ Test-AIModelId $_ })] [ArgumentCompleter({ param($cmd, $param, $word) $script:ValidModelIds | Where-Object { $_ -like "$word*" } })] [string]$Model = 'gemini-3.1-flash-lite-preview', [string]$ApiKey, [ValidateRange(1, 20)] [int]$BatchSize = 10 ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $TaxDir = Get-TaxonomyDir $PovFiles = @('accelerationist', 'safetyist', 'skeptic') if ($POV) { $PovFiles = @($POV) } $OtherPovs = @{ accelerationist = @('safetyist', 'skeptic') safetyist = @('accelerationist', 'skeptic') skeptic = @('accelerationist', 'safetyist') } $RequiredFields = @('epistemic_type', 'rhetorical_strategy', 'node_scope', 'intellectual_lineage', 'steelman_vulnerability') if ($Priority -eq 'full') { $RequiredFields += 'possible_fallacies' } # ── Collect nodes needing repair ────────────────────────────────────────── $NodesToFix = [System.Collections.Generic.List[PSObject]]::new() $TaxData = @{} foreach ($PovName in $PovFiles) { $FilePath = Join-Path $TaxDir "$PovName.json" if (-not (Test-Path $FilePath)) { continue } $Data = Get-Content $FilePath -Raw | ConvertFrom-Json $TaxData[$PovName] = $Data foreach ($Node in $Data.nodes) { if ($Node.PSObject.Properties['children'] -and $Node.children -and @($Node.children).Count -gt 0) { continue } if ($Category -and $Node.category -ne $Category) { continue } $HasGA = $Node.PSObject.Properties['graph_attributes'] -and $null -ne $Node.graph_attributes $MissingFields = [System.Collections.Generic.List[string]]::new() if (-not $HasGA) { foreach ($F in $RequiredFields) { $MissingFields.Add($F) } } else { $GA = $Node.graph_attributes foreach ($F in $RequiredFields) { if (-not $GA.PSObject.Properties[$F] -or $null -eq $GA.$F -or ($GA.$F -is [string] -and [string]::IsNullOrWhiteSpace($GA.$F)) -or ($GA.$F -is [array] -and @($GA.$F).Count -eq 0)) { $MissingFields.Add($F) } } } if ($MissingFields.Count -eq 0) { continue } # Priority filtering if ($Priority -eq 'critical') { $IsCritical = (-not $HasGA) -or $MissingFields.Contains('steelman_vulnerability') -or $MissingFields.Contains('intellectual_lineage') if (-not $IsCritical) { continue } } $NodesToFix.Add([PSCustomObject]@{ Node = $Node POV = $PovName MissingFields = @($MissingFields) HasGA = $HasGA }) } } # ── Summary ─────────────────────────────────────────────────────────────── $NoGACount = @($NodesToFix | Where-Object { -not $_.HasGA }).Count $SteelmanCount = @($NodesToFix | Where-Object { $_.MissingFields -contains 'steelman_vulnerability' }).Count $LineageCount = @($NodesToFix | Where-Object { $_.MissingFields -contains 'intellectual_lineage' }).Count $FallacyCount = @($NodesToFix | Where-Object { $_.MissingFields -contains 'possible_fallacies' }).Count Write-Host "=== Attribute Gaps (priority: $Priority) ===" -ForegroundColor Cyan Write-Host " Nodes to fix: $($NodesToFix.Count)" Write-Host " No graph_attributes: $NoGACount" Write-Host " Missing steelman_vulnerability: $SteelmanCount" Write-Host " Missing intellectual_lineage: $LineageCount" if ($Priority -eq 'full') { Write-Host " Missing possible_fallacies: $FallacyCount" } if ($NodesToFix.Count -eq 0) { Write-Host " Nothing to fix!" -ForegroundColor Green return } # Per-POV breakdown foreach ($PovName in $PovFiles) { $PovNodes = @($NodesToFix | Where-Object { $_.POV -eq $PovName }) if ($PovNodes.Count -gt 0) { Write-Host " $PovName`: $($PovNodes.Count) nodes" } } $TotalBatches = [Math]::Ceiling($NodesToFix.Count / $BatchSize) Write-Host " Batches: $TotalBatches ($BatchSize/batch)" if ($WhatIfPreference) { Write-Host "`n── Nodes to fix ────────────────────────────────" -ForegroundColor Yellow foreach ($Item in $NodesToFix) { $FieldList = $Item.MissingFields -join ', ' Write-Host " $($Item.Node.id) [$($Item.POV)/$($Item.Node.category)] — missing: $FieldList" -ForegroundColor Gray } return } # ── Resolve API key ─────────────────────────────────────────────────────── if ($Model -match '^gemini') { $Backend = 'gemini' } elseif ($Model -match '^claude') { $Backend = 'claude' } elseif ($Model -match '^openai') { $Backend = 'openai' } else { $Backend = 'gemini' } $ResolvedKey = Resolve-AIApiKey -ExplicitKey $ApiKey -Backend $Backend if ([string]::IsNullOrWhiteSpace($ResolvedKey)) { Write-Warning "No API key — cannot generate attributes" return } # ── Process in batches ──────────────────────────────────────────────────── $BatchNum = 0 $TotalFixed = 0 for ($i = 0; $i -lt $NodesToFix.Count; $i += $BatchSize) { $BatchNum++ $Batch = @($NodesToFix[$i..[Math]::Min($i + $BatchSize - 1, $NodesToFix.Count - 1)]) Write-Host "`nBatch $BatchNum/$TotalBatches ($($Batch.Count) nodes)..." -ForegroundColor Cyan $NodeDescriptions = ($Batch | ForEach-Object { $N = $_.Node $Missing = $_.MissingFields -join ', ' "- id: $($N.id) | pov: $($_.POV) | category: $($N.category) | label: $($N.label)`n description: $($N.description)`n missing: $Missing" }) -join "`n`n" $Prompt = @" Generate missing graph_attributes for these taxonomy nodes. For each node, provide ONLY the missing fields listed. FIELD DEFINITIONS: - epistemic_type: one of: empirical_claim, normative_prescription, causal_mechanism, definitional, predictive, methodological - rhetorical_strategy: comma-separated list from: techno_optimism, inevitability_framing, fear_appeal, evidence_based, rights_based, precautionary, pragmatic, systemic_critique, cost_benefit - node_scope: one of: narrow_technical, domain_specific, cross_domain, systemic - intellectual_lineage: array of 2-5 intellectual traditions (e.g., ["Effective Altruism", "Longtermism"]) - steelman_vulnerability: object with per-opponent-POV vulnerabilities: { "from_accelerationist": "1-2 sentences: strongest accelerationist critique", "from_safetyist": "1-2 sentences: strongest safetyist critique", "from_skeptic": "1-2 sentences: strongest skeptic critique" } (omit the node's own POV — e.g., for a safetyist node, include from_accelerationist and from_skeptic only) - possible_fallacies: array of 0-3 fallacy names from: appeal_to_authority, slippery_slope, false_dilemma, straw_man, appeal_to_fear, naturalistic_fallacy, is_ought_fallacy, composition_fallacy, hasty_generalization, tu_quoque, appeal_to_novelty, nirvana_fallacy (use empty array [] if none apply) NODES: $NodeDescriptions Return a JSON array. Each element: { "id": "node-id", "fields": { ...only the missing fields... } } No markdown fences, no explanation. "@ try { $Result = Invoke-AIApi -Prompt $Prompt -Model $Model -ApiKey $ResolvedKey ` -Temperature 0.3 -MaxTokens 8192 -JsonMode -TimeoutSec 60 if (-not $Result -or -not $Result.Text) { Write-Warning " No response for batch $BatchNum" continue } $CleanText = $Result.Text -replace '^\s*```json\s*', '' -replace '\s*```\s*$', '' $Responses = $CleanText | ConvertFrom-Json foreach ($Resp in @($Responses)) { $NodeId = $Resp.id $Fields = $Resp.fields # Find the matching item $Item = $Batch | Where-Object { $_.Node.id -eq $NodeId } | Select-Object -First 1 if (-not $Item) { continue } $Node = $Item.Node if ($PSCmdlet.ShouldProcess("$NodeId (fill $($Item.MissingFields -join ', '))", 'Set graph_attributes')) { # Ensure graph_attributes exists if (-not $Node.PSObject.Properties['graph_attributes'] -or $null -eq $Node.graph_attributes) { $Node | Add-Member -NotePropertyName 'graph_attributes' -NotePropertyValue ([PSCustomObject]@{}) -Force } $GA = $Node.graph_attributes foreach ($FieldName in $Item.MissingFields) { if ($Fields.PSObject.Properties[$FieldName] -and $null -ne $Fields.$FieldName) { if ($GA.PSObject.Properties[$FieldName]) { $GA.$FieldName = $Fields.$FieldName } else { $GA | Add-Member -NotePropertyName $FieldName -NotePropertyValue $Fields.$FieldName -Force } } } $TotalFixed++ Write-Host " FIXED $NodeId [$($Item.MissingFields -join ', ')]" -ForegroundColor Green } } } catch { Write-Warning " Batch $BatchNum failed: $($_.Exception.Message)" } if ($BatchNum -lt $TotalBatches) { Start-Sleep -Seconds 2 } } # ── Write modified files ────────────────────────────────────────────────── if ($TotalFixed -gt 0 -and -not $WhatIfPreference) { foreach ($PovName in $TaxData.Keys) { $FilePath = Join-Path $TaxDir "$PovName.json" $TaxData[$PovName] | ConvertTo-Json -Depth 20 | Set-Content -Path $FilePath -Encoding UTF8 } Write-Host "`nSaved taxonomy files" -ForegroundColor Green } Write-Host "`n=== SUMMARY ===" -ForegroundColor Cyan Write-Host " Nodes processed: $($NodesToFix.Count)" Write-Host " Fixed: $TotalFixed" } |