Private/OrganizationEdgesStore.ps1

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

# Organization actor-edges loader + module-scope cache (t/1526).
# Dot-sourced by AITriad.psm1 — do NOT export.
# Parallel to OrganizationsStore.ps1 (same shape / mtime invalidation pattern).

$script:OrganizationEdgesCache = $null
$script:OrganizationEdgesCacheTimestamp = $null
$script:OrganizationEdgesCachePath = $null

function Get-OrganizationEdgesFilePath {
    [CmdletBinding()]
    [OutputType([string])]
    param()
    $tax = Get-TaxonomyDir
    return Join-Path $tax 'organization_edges.json'
}

function Get-OrganizationEdgesStore {
    <#
    .SYNOPSIS
        Loads (or returns cached) organization_edges.json content.
    .DESCRIPTION
        Reads ../ai-triad-data/taxonomy/Origin/organization_edges.json, caches
        the parsed structure in $script:OrganizationEdgesCache, and invalidates
        on file mtime change. Returns the raw parsed object with fields:
        _schema_version, _doc, last_modified, edges[].
    .PARAMETER Force
        Bypass the cache and re-read from disk.
    .PARAMETER Path
        Override the source file path. Defaults to Get-OrganizationEdgesFilePath.
        Used by Test-OrganizationEdgeIntegrity so callers can validate a snapshot
        or fixture file without touching the live registry.
    #>

    [CmdletBinding()]
    param(
        [switch]$Force,
        [string]$Path
    )

    Set-StrictMode -Version Latest

    if ($Path) {
        $path = $Path
    } else {
        $path = Get-OrganizationEdgesFilePath
    }
    if (-not (Test-Path $path)) {
        throw (New-ActionableError `
            -Goal 'Load organization edges registry' `
            -Problem "organization_edges.json not found at $path" `
            -Location 'Get-OrganizationEdgesStore' `
            -NextSteps @(
                'Verify the data repo is present at ../ai-triad-data/',
                'Check .aitriad.json or $env:AI_TRIAD_DATA_ROOT for override',
                'Confirm taxonomy/Origin/organization_edges.json exists in the data repo'
            ))
    }

    $mtime = (Get-Item $path).LastWriteTimeUtc
    if (-not $Force -and $script:OrganizationEdgesCache -and $script:OrganizationEdgesCacheTimestamp -eq $mtime -and $script:OrganizationEdgesCachePath -eq $path) {
        return $script:OrganizationEdgesCache
    }

    try {
        $raw = Get-Content -Raw -Path $path -Encoding utf8
        $parsed = $raw | ConvertFrom-Json
    } catch {
        throw (New-ActionableError `
            -Goal 'Parse organization edges registry' `
            -Problem "Failed to parse organization_edges.json: $($_.Exception.Message)" `
            -Location 'Get-OrganizationEdgesStore' `
            -NextSteps @(
                'Validate JSON with: Get-Content organization_edges.json | ConvertFrom-Json',
                'Check for trailing commas or unbalanced brackets',
                'Restore from git history if the file was recently modified'
            ))
    }

    $script:OrganizationEdgesCache = $parsed
    $script:OrganizationEdgesCacheTimestamp = $mtime
    $script:OrganizationEdgesCachePath = $path
    return $parsed
}

function ConvertTo-OrganizationEdgeObject {
    <#
    .SYNOPSIS
        Converts a raw PSObject from organization_edges.json into a typed [OrganizationEdge].
    #>

    [CmdletBinding()]
    [OutputType([OrganizationEdge])]
    param(
        [Parameter(Mandatory)]
        [object]$Raw
    )
    Set-StrictMode -Version Latest

    $e = [OrganizationEdge]::new()
    $e.Source       = if ($Raw.PSObject.Properties['source']) { [string]$Raw.source } else { '' }
    $e.Target       = if ($Raw.PSObject.Properties['target']) { [string]$Raw.target } else { '' }
    $e.Type         = if ($Raw.PSObject.Properties['type']) { [string]$Raw.type } else { '' }
    $e.Rationale    = if ($Raw.PSObject.Properties['rationale']) { [string]$Raw.rationale } else { '' }
    $e.SourceRefs   = if ($Raw.PSObject.Properties['source_refs']) { @($Raw.source_refs | ForEach-Object { [string]$_ }) } else { @() }
    $e.Status       = if ($Raw.PSObject.Properties['status']) { [string]$Raw.status } else { 'approved' }
    $e.DiscoveredAt = if ($Raw.PSObject.Properties['discovered_at']) { [string]$Raw.discovered_at } else { '' }
    return $e
}

function Clear-OrganizationEdgesCache {
    <#
    .SYNOPSIS
        Invalidates the organization edges cache. Called by Import-OrganizationEdge after writes.
    #>

    [CmdletBinding()]
    param()
    $script:OrganizationEdgesCache = $null
    $script:OrganizationEdgesCacheTimestamp = $null
    $script:OrganizationEdgesCachePath = $null
}