Public/Get-IntellectualLineage.ps1

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

function Get-IntellectualLineage {
    <#
    .SYNOPSIS
        Queries intellectual lineage entries across taxonomy nodes.
    .DESCRIPTION
        Searches graph_attributes.intellectual_lineage on all taxonomy nodes
        and returns matching entries. Each entry links a taxonomy node to a
        philosophical movement, economic theory, scientific paradigm, or other
        intellectual tradition that informs that node's position.
 
        When lineage_categories.json is available, entries are enriched with
        Level 1 (broad) and Level 2 (specific) category classifications.
 
        Supports filtering by name/label wildcard, category, subcategory,
        and POV scope. Returns deduplicated results by default (unique by name).
    .PARAMETER Label
        One or more wildcard patterns matched against lineage entry names.
    .PARAMETER Category
        Filter by Level 1 category ID (e.g., 'ai-ml', 'economics', 'ethics-moral').
        Also accepts legacy per-node categories (e.g., 'economic_theory').
    .PARAMETER Subcategory
        Filter by Level 2 subcategory ID (e.g., 'ai-safety-alignment',
        'labor-political-economy').
    .PARAMETER POV
        Filter to a specific POV before scanning. Default: all POVs.
    .PARAMETER IncludeNodes
        Include the source taxonomy node ID and label in the output.
    .PARAMETER All
        Return all occurrences instead of deduplicating by name.
    .EXAMPLE
        Get-IntellectualLineage
        # All unique lineage entries across the taxonomy.
    .EXAMPLE
        Get-IntellectualLineage -Label '*Altruism*'
        # Entries matching 'Altruism' in name.
    .EXAMPLE
        Get-IntellectualLineage -Category economics
        # All entries in the Economics & Political Economy L1 category.
    .EXAMPLE
        Get-IntellectualLineage -Subcategory ai-safety-alignment
        # All entries in the AI Safety & Alignment L2 subcategory.
    .EXAMPLE
        Get-IntellectualLineage -POV skeptic -Label '*bias*'
        # Skeptic-scoped search for bias-related influences.
    .EXAMPLE
        Get-IntellectualLineage -Label '*commons*' -IncludeNodes
        # Shows which taxonomy nodes reference 'commons' traditions.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Position = 0)]
        [Alias('Name')]
        [string[]]$Label,

        [ArgumentCompleter({
            param($cmd, $param, $word)
            # L1 category IDs + legacy per-node categories
            @('ai-ml', 'techno-movements', 'ethics-moral', 'political-legal',
              'economics', 'social-behavioral', 'sts', 'formal-math',
              'risk-security', 'philosophy-epistemology', 'uncategorized',
              'academic_discipline', 'cultural_movement', 'economic_theory',
              'ethical_framework', 'legal_framework', 'philosophical_movement',
              'political_philosophy', 'scientific_paradigm', 'social_theory',
              'technology_movement', 'other') | Where-Object { $_ -like "$word*" }
        })]
        [string]$Category,

        [ArgumentCompleter({
            param($cmd, $param, $word)
            @('ai-safety-alignment', 'technology-society', 'labor-political-economy',
              'regulation-institutional-economics', 'legal-theory-applied-ethics',
              'democratic-governance', 'critical-social-theory', 'science-epistemology',
              'cognitive-behavioral-science', 'information-theory-cybernetics',
              'existential-risk-futures', 'game-decision-theory', 'digital-rights-governance',
              'tech-movements-open-source', 'philosophy-of-mind', 'consciousness-transhumanism',
              'environmental-sustainability', 'systems-complexity', 'surveillance-security',
              'political-economy-development', 'economic-inequality', 'neuroethics-bioethics',
              'media-communication', 'organizational-theory', 'military-defense',
              'international-relations', 'innovation-entrepreneurship', 'education-learning',
              'data-science-statistics', 'rights-justice', 'behavioral-economics',
              'social-epistemology', 'public-health', 'theology-religion',
              'art-design-creativity', 'feminist-gender-theory', 'postcolonial-theory',
              'metaphysics-ontology', 'disability-studies', 'uncategorized'
            ) | Where-Object { $_ -like "$word*" }
        })]
        [string]$Subcategory,

        [ArgumentCompleter({ param($cmd, $param, $word) @('accelerationist','safetyist','skeptic','situations') | Where-Object { $_ -like "$word*" } })]
        [string]$POV = '*',

        [switch]$IncludeNodes,

        [switch]$All
    )

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

    Assert-TaxonomyCacheFresh

    # ── Load lineage_categories.json for L1/L2 mapping ───────────────────
    $L1Lookup    = @{}  # L1 id → label
    $L2Lookup    = @{}  # L2 id → { label, l1_parent }
    $NameMapping = $null  # name → { l1, l2 }
    $HasL2Data   = $false

    $CatFile = Get-TaxonomyDir 'lineage_categories.json'
    if (Test-Path $CatFile) {
        try {
            $CatData = Get-Content -Raw -Path $CatFile | ConvertFrom-Json -AsHashtable
            if ($CatData.ContainsKey('categories')) {
                foreach ($c in $CatData['categories']) {
                    $L1Lookup[$c['id']] = $c['label']
                }
            }
            if ($CatData.ContainsKey('level2_categories')) {
                $HasL2Data = $true
                foreach ($c in $CatData['level2_categories']) {
                    $L2Lookup[$c['id']] = @{ label = $c['label']; l1_parent = $c['l1_parent'] }
                }
            }
            if ($CatData.ContainsKey('mapping')) {
                $NameMapping = $CatData['mapping']
            }
        } catch {
            Write-Verbose "Could not load lineage_categories.json: $_"
        }
    }

    $HasLabel    = ($null -ne $Label) -and ($Label.Length -gt 0)
    $HasCategory = -not [string]::IsNullOrWhiteSpace($Category)
    $HasSubcat   = -not [string]::IsNullOrWhiteSpace($Subcategory)
    $Seen        = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
    $Results     = [System.Collections.Generic.List[PSObject]]::new()

    foreach ($Key in $script:TaxonomyData.Keys) {
        if ($Key -notlike $POV.ToLower()) { continue }
        $Entry = $script:TaxonomyData[$Key]

        foreach ($Node in $Entry.nodes) {
            if (-not $Node.PSObject.Properties['graph_attributes']) { continue }
            $GA = $Node.graph_attributes
            if (-not $GA.PSObject.Properties['intellectual_lineage']) { continue }

            foreach ($Item in $GA.intellectual_lineage) {
                # Handle bare strings (unenriched entries)
                if ($Item -is [string]) {
                    $EntryName = $Item
                    $Desc = $null
                    $Url  = $null
                    $NodeCat = $null
                } else {
                    $EntryName = if ($Item.PSObject.Properties['name']) { $Item.name } else { "$Item" }
                    $Desc = if ($Item.PSObject.Properties['description']) { $Item.description } else { $null }
                    $Url  = if ($Item.PSObject.Properties['url']) { $Item.url } else { $null }
                    $NodeCat = if ($Item.PSObject.Properties['category']) { $Item.category } else { $null }
                }

                # Resolve L1/L2 from mapping
                $L1Id = $null; $L1Label = $null
                $L2Id = $null; $L2Label = $null
                if ($null -ne $NameMapping -and $NameMapping.ContainsKey($EntryName)) {
                    $Map = $NameMapping[$EntryName]
                    if ($Map -is [hashtable] -or ($null -ne $Map -and $Map.PSObject.Properties['l1'])) {
                        $L1Id = if ($Map -is [hashtable]) { $Map['l1'] } else { $Map.l1 }
                        $L2Id = if ($Map -is [hashtable]) { $Map['l2'] } else { $Map.l2 }
                    } elseif ($Map -is [string]) {
                        # Legacy flat string mapping
                        $L1Id = $Map
                    }
                    if ($L1Id -and $L1Lookup.ContainsKey($L1Id)) { $L1Label = $L1Lookup[$L1Id] }
                    if ($L2Id -and $L2Lookup.ContainsKey($L2Id)) { $L2Label = $L2Lookup[$L2Id].label }
                }

                # Label filter
                if ($HasLabel) {
                    $Match = $false
                    foreach ($Pat in $Label) {
                        if ($EntryName -like $Pat) { $Match = $true; break }
                    }
                    if (-not $Match) { continue }
                }

                # Category filter — matches L1 id OR legacy per-node category
                if ($HasCategory -and $L1Id -ne $Category -and $NodeCat -ne $Category) { continue }

                # Subcategory filter — matches L2 id
                if ($HasSubcat -and $L2Id -ne $Subcategory) { continue }

                # Dedup by name unless -All
                if (-not $All -and -not $Seen.Add($EntryName)) { continue }

                $Obj = [PSCustomObject]@{
                    PSTypeName  = 'AITriad.IntellectualLineage'
                    Name        = $EntryName
                    L1Category  = if ($L1Label) { $L1Label } else { $NodeCat }
                    L2Category  = $L2Label
                    Description = $Desc
                    Url         = $Url
                }

                if ($IncludeNodes) {
                    $Obj | Add-Member -NotePropertyName NodeId    -NotePropertyValue $Node.id
                    $Obj | Add-Member -NotePropertyName NodeLabel -NotePropertyValue $Node.label
                    $Obj | Add-Member -NotePropertyName NodePOV   -NotePropertyValue $Key
                }

                $Results.Add($Obj)
            }
        }
    }

    if ($Results.Count -eq 0) {
        $Terms = @()
        if ($HasLabel)    { $Terms += ($Label | ForEach-Object { "Label='$_'" }) }
        if ($HasCategory) { $Terms += "Category='$Category'" }
        if ($HasSubcat)   { $Terms += "Subcategory='$Subcategory'" }
        if ($Terms.Count -gt 0) {
            Write-Warning "No lineage entries matched: $($Terms -join ', ')"
        } else {
            Write-Warning 'No intellectual lineage data found in taxonomy.'
        }
        return
    }

    $Results | Sort-Object Name
}