Public/Import-Organization.ps1
|
# Copyright (c) 2026 Jeffrey Snover. All rights reserved. # Licensed under the MIT License. See LICENSE file in the project root. function Import-Organization { <# .SYNOPSIS Create or update an organization record in organizations.json. .DESCRIPTION Adds a new organization if -Id does not exist, otherwise updates the existing record. Runs Test-OrganizationIntegrity after the write and rolls back if the integrity check fails. Writes are atomic: staged to a temp file, then File.Move'd over the target, so a mid-write crash leaves the previous file intact. Invalidates the module-scope cache so subsequent reads see the change. .PARAMETER Id Organization id (org-NNN). Must match ^org-\d{3,}$. .PARAMETER Name Organization full name. .PARAMETER Type Organization type enum. .PARAMETER InputObject Alternative: supply a full pre-built hashtable or PSCustomObject that will be spliced into the record. Fields override the individual parameters above. .PARAMETER WhatIf Show the planned write without touching the file. .OUTPUTS [Organization] .EXAMPLE Import-Organization -Id org-026 -Name 'New Org' -Type advocacy .EXAMPLE $rec = @{ id = 'org-027'; name = 'X'; type = 'academic'; pov_alignment = @{ safetyist = @{ score = 0.5; rationale = 'r' } } } Import-Organization -InputObject $rec .LINK Show-AITriadHelp .LINK Get-Organization .LINK Find-OrganizationByPOV .LINK Find-OrganizationByTopic .LINK Get-OrganizationStakeholders .LINK Compare-OrganizationPositions #> [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'Fields')] [OutputType('Organization')] param( [Parameter(Mandatory, ParameterSetName='Fields')] [ValidatePattern('^org-\d{3,}$')] [string]$Id, [Parameter(Mandatory, ParameterSetName='Fields')] [string]$Name, [Parameter(Mandatory, ParameterSetName='Fields')] [ValidateSet('think_tank','advocacy','regulatory','academic','corporate','intergovernmental','civil_society','standards_body','research_lab')] [string]$Type, [Parameter(ParameterSetName='Fields')] [string]$ShortName = '', [Parameter(ParameterSetName='Fields')] [string]$Description = '', [Parameter(ParameterSetName='Fields')] [string]$Url = '', [Parameter(Mandatory, ParameterSetName='InputObject')] [object]$InputObject ) Set-StrictMode -Version Latest $path = Get-OrganizationsFilePath $store = Get-OrganizationsStore -Force # Build the incoming record if ($PSCmdlet.ParameterSetName -eq 'InputObject') { $incoming = $InputObject if (-not $incoming.PSObject.Properties['id']) { throw (New-ActionableError ` -Goal 'Import organization' ` -Problem '-InputObject is missing required field: id' ` -Location 'Import-Organization' ` -NextSteps @('Add id = "org-NNN" to the input record')) } $incomingId = [string]$incoming.id } else { $now = (Get-Date).ToString('yyyy-MM-dd') $incoming = [PSCustomObject]@{ id = $Id name = $Name short_name = $ShortName type = $Type description = $Description url = $Url status = 'active' pov_alignment = [PSCustomObject]@{} topic_engagement = @() policy_engagement = @() key_figures = @() external_links = @() source_refs = @() tags = @() created_at = $now last_modified = $now } $incomingId = $Id } # Upsert if ($store.PSObject.Properties['organizations']) { $existing = @($store.organizations) } else { $existing = @() } $index = -1 for ($i = 0; $i -lt @($existing).Count; $i++) { $rid = if ($existing[$i].PSObject.Properties['id']) { [string]$existing[$i].id } else { '' } if ($rid -eq $incomingId) { $index = $i; break } } if ($index -ge 0) { # t/1555 — preserve/refresh assessed_at on pov_alignment across upserts. # Rule: if pov_alignment scores/rationales changed, stamp today; else # carry over the existing assessed_at so unrelated updates don't churn # the staleness signal. $incoming = Set-OrgAssessedAt -Existing $existing[$index] -Incoming $incoming # t/1560 — preserve pov_alignment_derived across partial upserts. # A caller who edits an unrelated field without carrying forward the # derived block would otherwise wipe it (this cmdlet replaces the # whole record). Only Invoke-OrgDerivedCampScores rewrites the block. $incoming = Set-OrgPreserveDerived -Existing $existing[$index] -Incoming $incoming $existing[$index] = $incoming Write-Verbose "Updating existing organization: $incomingId" } else { # Create — carry through whatever the caller supplied (pov_alignment # may already have assessed_at from an external source; if not, they # can add one on a later upsert). Nothing stamped implicitly. $existing += $incoming Write-Verbose "Adding new organization: $incomingId" } if ($store.PSObject.Properties['organizations']) { $store.organizations = @($existing) } else { Add-Member -InputObject $store -MemberType NoteProperty -Name 'organizations' -Value (@($existing)) } if ($store.PSObject.Properties['org_count']) { $store.org_count = @($existing).Count } else { Add-Member -InputObject $store -MemberType NoteProperty -Name 'org_count' -Value (@($existing).Count) } $stamp = (Get-Date).ToString('yyyy-MM-dd') if ($store.PSObject.Properties['last_modified']) { $store.last_modified = $stamp } else { Add-Member -InputObject $store -MemberType NoteProperty -Name 'last_modified' -Value $stamp } if (-not $PSCmdlet.ShouldProcess($path, "Upsert organization $incomingId")) { return (ConvertTo-OrganizationObject -Raw $incoming) } # Atomic write: temp + File.Move $temp = "$path.tmp" $json = $store | ConvertTo-Json -Depth 12 try { Set-Content -Path $temp -Value $json -Encoding utf8NoBOM [System.IO.File]::Move($temp, $path, $true) } catch { if (Test-Path $temp) { Remove-Item $temp -Force -ErrorAction SilentlyContinue } throw (New-ActionableError ` -Goal 'Write organizations.json' ` -Problem "Failed to write registry: $($_.Exception.Message)" ` -Location 'Import-Organization' ` -NextSteps @( "Verify write access to $path", 'Check disk space', 'Restore from git if the file is corrupted' )) } Clear-OrganizationsCache # Post-write validation — rollback recommendation only, we don't auto-revert # since git is the safety net for data-repo files. $report = Test-OrganizationIntegrity if (-not $report.Pass) { Write-Warning "Post-write integrity check FAILED: $($report.Errors) errors, $($report.Warnings) warnings. Review with: (Test-OrganizationIntegrity).Issues" } return (ConvertTo-OrganizationObject -Raw $incoming) } |