Public/Invoke-PolicyRefinement.ps1
|
# Copyright (c) 2026 Jeffrey Snover. All rights reserved. # Licensed under the MIT License. See LICENSE file in the project root. function Invoke-PolicyRefinement { <# .SYNOPSIS Refines canonical policy action text using LLM analysis of all framings. .DESCRIPTION Finds all policies with member_count > 1 (i.e., referenced by multiple taxonomy nodes), collects the POV-specific framings from every referencing node, and asks an LLM to generate a POV-neutral canonical action statement (5-15 words). In DryRun mode, displays the prompt and current vs proposed text without calling the API. In normal mode, calls the API, updates policy_actions.json, and cascades the refined action text to all referencing nodes. .PARAMETER Model AI model to use. Defaults to AI_MODEL env var, then "gemini-2.5-flash". .PARAMETER ApiKey AI API key. If omitted, resolved via backend-specific env var or AI_API_KEY. .PARAMETER DryRun Show prompts and current text without calling the API. .PARAMETER PassThru Return a summary object with refinement details. .EXAMPLE Invoke-PolicyRefinement -DryRun .EXAMPLE Invoke-PolicyRefinement -Model 'claude-sonnet-4-20250514' .EXAMPLE Invoke-PolicyRefinement -PassThru #> [CmdletBinding(SupportsShouldProcess)] param( [ValidateScript({ Test-AIModelId $_ })] [ArgumentCompleter({ param($cmd, $param, $word) $script:ValidModelIds | Where-Object { $_ -like "$word*" } })] [string]$Model = '', [string]$ApiKey = '', [switch]$DryRun, [switch]$PassThru ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' if (-not $Model) { $Model = if ($env:AI_MODEL) { $env:AI_MODEL } else { 'gemini-2.5-flash' } } # -- Validate environment -------------------------------------------------- Write-Step 'Validating environment' $TaxDir = Get-TaxonomyDir $RegistryPath = Join-Path $TaxDir 'policy_actions.json' if (-not (Test-Path $RegistryPath)) { Write-Fail 'Policy registry not found. Run Update-PolicyRegistry -Fix first.' throw 'Policy registry not found' } if (-not $DryRun) { $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' } } # -- Load registry and taxonomy files -------------------------------------- Write-Step 'Loading policy registry and taxonomy' $Registry = Get-Content -Raw -Path $RegistryPath | ConvertFrom-Json Write-OK "Registry loaded: $($Registry.policies.Count) policies" $PovFiles = @('accelerationist', 'safetyist', 'skeptic', 'cross-cutting') $TaxData = @{} foreach ($PovKey in $PovFiles) { $FilePath = Join-Path $TaxDir "$PovKey.json" if (Test-Path $FilePath) { $TaxData[$PovKey] = Get-Content -Raw -Path $FilePath | ConvertFrom-Json } } # -- Find multi-member policies -------------------------------------------- Write-Step 'Finding policies with multiple framings' $MultiPolicies = @($Registry.policies | Where-Object { $_.member_count -gt 1 }) if ($MultiPolicies.Count -eq 0) { Write-OK 'No policies with member_count > 1 found. Nothing to refine.' if ($PassThru) { return [PSCustomObject]@{ PoliciesFound = 0 Refined = 0 Failed = 0 } } return } Write-OK "Found $($MultiPolicies.Count) policies with multiple framings" # -- Collect framings for each policy -------------------------------------- Write-Step 'Collecting framings from referencing nodes' $PolicyFramings = @{} # policy_id -> list of { NodeId, POV, Action, Framing } foreach ($PovKey in $PovFiles) { if (-not $TaxData.ContainsKey($PovKey)) { continue } foreach ($Node in $TaxData[$PovKey].nodes) { if (-not $Node.PSObject.Properties['graph_attributes'] -or $null -eq $Node.graph_attributes) { continue } if (-not $Node.graph_attributes.PSObject.Properties['policy_actions']) { continue } foreach ($PA in $Node.graph_attributes.policy_actions) { $Pid = if ($PA.PSObject.Properties['policy_id']) { $PA.policy_id } else { $null } if (-not $Pid) { continue } if (-not $PolicyFramings.ContainsKey($Pid)) { $PolicyFramings[$Pid] = [System.Collections.Generic.List[object]]::new() } $PolicyFramings[$Pid].Add([PSCustomObject]@{ NodeId = $Node.id POV = $PovKey Action = $PA.action Framing = if ($PA.PSObject.Properties['framing']) { $PA.framing } else { '' } }) } } } # -- Process each multi-member policy -------------------------------------- $Refined = 0 $Failed = 0 $Results = [System.Collections.Generic.List[object]]::new() foreach ($Policy in $MultiPolicies) { $Pid = $Policy.id $CurrentAction = $Policy.action if (-not $PolicyFramings.ContainsKey($Pid)) { Write-Warn "$Pid`: no framings found in taxonomy files" $Failed++ continue } $Framings = $PolicyFramings[$Pid] Write-Step "$Pid`: $CurrentAction ($($Framings.Count) framings)" # Build the refinement prompt $FramingBlock = ($Framings | ForEach-Object { "- Node $($_.NodeId) [$($_.POV)]: action=`"$($_.Action)`" framing=`"$($_.Framing)`"" }) -join "`n" $Prompt = @" You are a policy language editor. Your task is to write a single canonical policy action statement that is POV-neutral (not biased toward any camp: accelerationist, safetyist, or skeptic). CURRENT canonical action: "$CurrentAction" This policy ($Pid) is referenced by $($Framings.Count) nodes across different POVs. Here are all the framings: $FramingBlock INSTRUCTIONS: 1. Synthesize a single POV-neutral canonical action statement. 2. The statement must be 5-15 words, concrete, and actionable. 3. Do not favor any single POV's framing. Find the neutral common ground. 4. Return ONLY a JSON object: {"refined_action": "<your 5-15 word statement>"} 5. If the current action is already neutral and well-formed, return it unchanged. "@ # -- DryRun: show prompt and skip API call -- if ($DryRun) { Write-Host '' Write-Host "--- $Pid ---" -ForegroundColor Cyan Write-Host " Current : $CurrentAction" -ForegroundColor Yellow Write-Host " Framings:" -ForegroundColor Gray foreach ($F in $Framings) { Write-Host " $($F.NodeId) [$($F.POV)]: $($F.Action)" -ForegroundColor DarkGray if ($F.Framing) { Write-Host " framing: $($F.Framing.Substring(0, [Math]::Min(100, $F.Framing.Length)))" -ForegroundColor DarkGray } } Write-Host " Prompt length: ~$($Prompt.Length) chars" -ForegroundColor Gray continue } # -- Call LLM -- try { $AIResult = Invoke-AIApi ` -Prompt $Prompt ` -Model $Model ` -ApiKey $ResolvedKey ` -Temperature 0.1 ` -MaxTokens 512 ` -TimeoutSec 120 $ResponseText = $AIResult.Text if (-not $ResponseText) { Write-Warn "$Pid`: empty API response" $Failed++ continue } # Strip markdown fences and extract JSON $ResponseText = $ResponseText -replace '(?s)^\s*```json\s*', '' -replace '(?s)\s*```\s*$', '' # Find the first complete JSON object (handles multi-line values) $JsonMatch = [regex]::Match($ResponseText, '(?s)\{[^{}]*"refined_action"\s*:\s*"[^"]*"[^{}]*\}') if (-not $JsonMatch.Success) { # Fallback: try to find any JSON object $JsonMatch = [regex]::Match($ResponseText, '(?s)\{.*?\}') } if (-not $JsonMatch.Success) { Write-Warn "$Pid`: no JSON object found in response: $($ResponseText.Substring(0, [Math]::Min(100, $ResponseText.Length)))" $Failed++ continue } $Parsed = $JsonMatch.Value | ConvertFrom-Json if (-not $Parsed.PSObject.Properties['refined_action'] -or [string]::IsNullOrWhiteSpace($Parsed.refined_action)) { Write-Warn "$Pid`: LLM returned empty or missing refined_action" $Failed++ continue } $RefinedAction = $Parsed.refined_action.Trim() } catch { Write-Fail "$Pid`: API call or parse failed -- $_" $Failed++ continue } Write-Info "$Pid`: `"$CurrentAction`" -> `"$RefinedAction`"" # -- Update policy_actions.json -- if ($PSCmdlet.ShouldProcess("$Pid in policy_actions.json", "Update action to '$RefinedAction'")) { $Policy.action = $RefinedAction } # -- Cascade to all referencing nodes -- foreach ($F in $Framings) { $PovKey = $F.POV if (-not $TaxData.ContainsKey($PovKey)) { continue } foreach ($Node in $TaxData[$PovKey].nodes) { if ($Node.id -ne $F.NodeId) { continue } if (-not $Node.PSObject.Properties['graph_attributes'] -or $null -eq $Node.graph_attributes) { continue } if (-not $Node.graph_attributes.PSObject.Properties['policy_actions']) { continue } foreach ($PA in $Node.graph_attributes.policy_actions) { if ($PA.PSObject.Properties['policy_id'] -and $PA.policy_id -eq $Pid) { if ($PSCmdlet.ShouldProcess("$($Node.id) [$PovKey]", "Update action text for $Pid")) { $PA.action = $RefinedAction } } } } } $Refined++ $Results.Add([PSCustomObject]@{ PolicyId = $Pid OriginalAction = $CurrentAction RefinedAction = $RefinedAction FramingCount = $Framings.Count }) } # -- Write updated files --------------------------------------------------- if (-not $DryRun -and $Refined -gt 0) { Write-Step 'Writing updated files' # Save registry if ($PSCmdlet.ShouldProcess($RegistryPath, 'Write updated policy registry')) { $Registry | ConvertTo-Json -Depth 10 | Set-Content -Path $RegistryPath -Encoding UTF8 Write-OK "Registry saved: $($Registry.policies.Count) policies" } # Save taxonomy files foreach ($PovKey in $PovFiles) { if (-not $TaxData.ContainsKey($PovKey)) { continue } $FilePath = Join-Path $TaxDir "$PovKey.json" if ($PSCmdlet.ShouldProcess($FilePath, 'Write updated taxonomy file')) { $TaxData[$PovKey] | ConvertTo-Json -Depth 20 | Set-Content -Path $FilePath -Encoding UTF8 Write-OK "Saved $PovKey" } } } # -- Summary --------------------------------------------------------------- Write-Host '' Write-Host '=== Policy Refinement Complete ===' -ForegroundColor Cyan Write-Host " Multi-member policies : $($MultiPolicies.Count)" -ForegroundColor White Write-Host " Refined : $Refined" -ForegroundColor $(if ($Refined -gt 0) { 'Green' } else { 'Gray' }) Write-Host " Failed : $Failed" -ForegroundColor $(if ($Failed -gt 0) { 'Red' } else { 'Green' }) if ($DryRun) { Write-Host ' Mode : DRY RUN (no changes made)' -ForegroundColor Yellow } Write-Host '' if ($PassThru) { [PSCustomObject]@{ PoliciesFound = $MultiPolicies.Count Refined = $Refined Failed = $Failed Details = $Results } } } |