Private/Resolve-EdgeType.ps1

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

# Edge-type validation + mapping post-processing (t/1093).
# Dot-sourced by AITriad.psm1 — do NOT export.

# Authoritative 8-type canonical vocabulary. Update here if the vocabulary
# changes; the consumers (Invoke-EdgeDiscovery, Set-Edge, prompts) read from
# this single source of truth.
$script:CanonicalEdgeTypes = @(
    'SUPPORTS',
    'CONTRADICTS',
    'WEAKENS',
    'TENSION_WITH',
    'RESPONDS_TO',
    'ASSUMES',
    'INTERPRETS',
    'CONVERGES_WITH'
)

# Reclassify table — LLM-hallucinated synonyms that genuinely map to SUPPORTS.
# Other unknown types are dropped (logged + discarded), not silently mapped.
$script:EdgeTypeReclassify = @{
    MOTIVATES   = 'SUPPORTS'
    COMPLEMENTS = 'SUPPORTS'
    ENABLES     = 'SUPPORTS'
}

function Resolve-EdgeType {
    <#
    .SYNOPSIS
        Validates and maps an edge type to the canonical 8-type vocabulary.
    .DESCRIPTION
        Pure stateless function. Returns an EdgeTypeResolution object with one
        of three Action values:
          - 'accept' — type is canonical; .Type is the normalized (upper) form
          - 'reclassify' — type is a known synonym; .Type is the canonical mapping
          - 'drop' — type is deprecated or unrecognized; caller discards
                           and logs .Reason
 
        Dedup (skip a reclassified edge that duplicates an existing one) is
        the caller's responsibility, not this function's — the helper stays
        stateless so it composes cleanly with batch + manual write paths.
    .PARAMETER Type
        The proposed edge type (any case, any string).
    .EXAMPLE
        $r = Resolve-EdgeType -Type 'supports'
        # $r.Action = 'accept', $r.Type = 'SUPPORTS'
    .EXAMPLE
        $r = Resolve-EdgeType -Type 'MOTIVATES'
        # $r.Action = 'reclassify', $r.Type = 'SUPPORTS', $r.Reason = 'MOTIVATES → SUPPORTS'
    .EXAMPLE
        $r = Resolve-EdgeType -Type 'CITES'
        # $r.Action = 'drop', $r.Reason = 'CITES is deprecated; no canonical mapping'
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [AllowEmptyString()]
        [AllowNull()]
        [string]$Type
    )

    Set-StrictMode -Version Latest

    $Result = [EdgeTypeResolution]::new()

    if ([string]::IsNullOrWhiteSpace($Type)) {
        $Result.Action = 'drop'
        $Result.Reason = 'edge type is null or empty'
        return $Result
    }

    $Upper = $Type.ToUpperInvariant()

    if ($script:CanonicalEdgeTypes -contains $Upper) {
        $Result.Action = 'accept'
        $Result.Type   = $Upper
        return $Result
    }

    if ($script:EdgeTypeReclassify.ContainsKey($Upper)) {
        $Mapped = $script:EdgeTypeReclassify[$Upper]
        $Result.Action = 'reclassify'
        $Result.Type   = $Mapped
        $Result.Reason = "$Upper → $Mapped"
        return $Result
    }

    $Result.Action = 'drop'
    $Result.Reason = "$Upper is not in canonical vocabulary and has no reclassify mapping"
    return $Result
}

function Get-CanonicalEdgeType {
    # Helper for tests + consumers to read the canonical list. Returns a fresh array.
    [CmdletBinding()]
    param()
    return @($script:CanonicalEdgeTypes)
}