Modules/businessdev.ALbuild.Apps/Public/Invoke-BcCallGraphQuery.ps1

function Invoke-BcCallGraphQuery {
    <#
    .SYNOPSIS
        Answers a reachability question over a call graph with 'al graph query' (who calls X, what does X reach).
 
    .DESCRIPTION
        Selectors follow al graph: 'obj:<object name>', 'app:<app id>', 'ns:<pattern>', 'scope:cloud',
        'access:public', and the aliases 'debuggable', 'onprem-surface', 'elevation'; '!' or 'not:' negates,
        '+' is AND and ',' is OR. With From alone the callees are walked, with To alone the callers; with both
        and -Paths the concrete paths between them are listed.
 
    .PARAMETER GraphPath
        The call graph folder (or its graph.jsonl).
 
    .PARAMETER From
        Source selector.
 
    .PARAMETER To
        Target selector.
 
    .PARAMETER Direction
        callees, callers or both. Default: callees for From, callers for To.
 
    .PARAMETER Depth
        Maximum traversal depth (0 = unbounded).
 
    .PARAMETER EdgeKinds
        Edge kinds to follow: Direct, Event, Interface, Run, Trigger.
 
    .PARAMETER Paths
        List the concrete From -> To paths.
 
    .PARAMETER MaxPaths
        Cap for -Paths. Default 1000.
 
    .PARAMETER ResolvedOnly
        Follow statically resolved edges only (drops over-approximated interface edges).
 
    .PARAMETER Exclude
        Prune nodes matching this selector, e.g. 'ns:*Test*'.
 
    .PARAMETER AlToolPath
        The 'al' executable. Default: an installed AL Tool with the graph command.
 
    .EXAMPLE
        Invoke-BcCallGraphQuery -GraphPath .albuild/callgraph -To 'obj:bdev.BNK API Client' -Depth 1
 
    .OUTPUTS
        PSCustomObject: Count, Results (one line per node or path), Output (the raw text).
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory)] [string] $GraphPath,
        [string] $From,
        [string] $To,
        [ValidateSet('', 'callees', 'callers', 'both')] [string] $Direction = '',
        [ValidateRange(0, [int]::MaxValue)] [int] $Depth = 0,
        [string[]] $EdgeKinds = @(),
        [switch] $Paths,
        [ValidateRange(1, [int]::MaxValue)] [int] $MaxPaths = 1000,
        [switch] $ResolvedOnly,
        [string] $Exclude,
        [string] $AlToolPath
    )

    if (-not $From -and -not $To) { throw 'Pass -From, -To or both.' }
    $graph = Resolve-BcCallGraphPath -Path $GraphPath -Require
    $al = $AlToolPath
    if (-not $al) { $al = (Install-BcAlTool -RuntimeVersion '18.0').Path }

    $arguments = New-Object System.Collections.Generic.List[string]
    $arguments.AddRange([string[]]@('graph', 'query', '--graph', $graph.GraphPath))
    if ($From) { $arguments.AddRange([string[]]@('--from', $From)) }
    if ($To) { $arguments.AddRange([string[]]@('--to', $To)) }
    if ($Direction) { $arguments.AddRange([string[]]@('--direction', $Direction)) }
    if ($Depth -gt 0) { $arguments.AddRange([string[]]@('--depth', "$Depth")) }
    if ($EdgeKinds.Count -gt 0) { $arguments.AddRange([string[]]@('--edge-kinds', ($EdgeKinds -join ','))) }
    if ($Paths) { $arguments.Add('--paths'); $arguments.AddRange([string[]]@('--max-paths', "$MaxPaths")) }
    if ($ResolvedOnly) { $arguments.Add('--resolved-only') }
    if ($Exclude) { $arguments.AddRange([string[]]@('--exclude', $Exclude)) }

    $run = Invoke-ALbuildProcess -FilePath $al -Arguments $arguments.ToArray() -PassThru
    $text = "$($run.StdOut)"
    if (-not $run.Success -or $text -match 'Unhandled exception') {
        $detail = if ("$($run.StdErr)".Trim()) { $run.StdErr } else { $text }
        throw "al graph query failed: $(("$detail" -split "`r?`n" | Where-Object { $_.Trim() } | Select-Object -First 3) -join ' ')"
    }
    $lines = @($text -split "`r?`n" | Where-Object { $_.Trim() })
    $count = $null
    if ($lines.Count -gt 0 -and $lines[0] -match '^(\d+)\s') { $count = [int]$Matches[1] }
    [PSCustomObject]@{
        Count   = $count
        Results = @($lines | Select-Object -Skip 1 | ForEach-Object { $_.Trim() })
        Output  = $text.TrimEnd()
    }
}