Modules/businessdev.ALbuild.Apps/Private/Format-BcCallGraphSummary.ps1

function Format-BcCallGraphSummary {
    <#
    .SYNOPSIS
        Renders the Markdown build summary of a call graph analysis.
 
    .PARAMETER Analysis
        The result of Invoke-BcCallGraphAnalysis.
 
    .PARAMETER Gate
        The gate result (Passed, Failures) or $null when no gate is configured.
 
    .PARAMETER MaxItems
        Rows per list in the summary; the HTML report has them all.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory)] $Analysis,
        $Gate,
        [int] $MaxItems = 10,
        # Point at the build's Call graph tab (pipeline runs only).
        [switch] $PipelineTab
    )

    $m = $Analysis['metrics']
    $b = $Analysis['baselineMetrics']
    $f = $Analysis['findings']
    $imp = $Analysis['impact']
    $sb = New-Object System.Text.StringBuilder
    $own = @($Analysis['apps'] | Where-Object { $_['role'] -eq 'own' } | ForEach-Object { $_['name'] })
    [void]$sb.AppendLine("## Call graph: $($own -join ', ')")
    [void]$sb.AppendLine()
    if ($Analysis['baseline']) { [void]$sb.AppendLine("Compared with $($Analysis['baseline'])."); [void]$sb.AppendLine() }

    $noSource = @($Analysis['apps'] | Where-Object { $_['role'] -eq 'dependency' -and -not $_['hasSource'] } | ForEach-Object { $_['name'] })
    if ($noSource.Count -gt 0) {
        [void]$sb.AppendLine("> **Note:** no source for $($noSource -join ', '). Calls into these apps, and calls passing their types, are missing; dead code and test reach are less precise.")
        [void]$sb.AppendLine()
    }
    if ($Gate) {
        if ($Gate.Passed) { [void]$sb.AppendLine('**Gate:** passed.') }
        else { [void]$sb.AppendLine("**Gate failed:** $($Gate.Failures -join '; ')") }
        [void]$sb.AppendLine()
    }

    $rows = @(
        @('Objects', 'objects'), @('Methods', 'methods'), @('Triggers', 'triggers'), @('Calls', 'edges'),
        @('Test methods', 'testMethods'), @('Not reached by tests', 'notReachedByTests'), @('Not reached by tests (%)', 'notReachedPercent'),
        @('Obsolete calls', 'obsoleteCalls'), @('Dead methods', 'deadMethods'), @('Calls into dependencies', 'dependencyCalls'),
        @('Event publishers', 'eventPublishers'), @('Event subscribers', 'eventSubscribers'), @('Interface implementations', 'interfaceImplementations')
    )
    if ($b) { [void]$sb.AppendLine('| Metric | This build | Baseline | Change |'); [void]$sb.AppendLine('|---|---:|---:|---:|') }
    else { [void]$sb.AppendLine('| Metric | This build |'); [void]$sb.AppendLine('|---|---:|') }
    foreach ($r in $rows) {
        $v = $m[$r[1]]
        $shown = if ($null -eq $v) { '-' } else { "$v" }
        if ($b) {
            $old = $b[$r[1]]
            $delta = ''
            if ($null -ne $v -and $null -ne $old) {
                $d = [math]::Round(([double]$v - [double]$old), 1)
                $delta = if ($d -gt 0) { "+$d" } elseif ($d -lt 0) { "$d" } else { '' }
            }
            [void]$sb.AppendLine("| $($r[0]) | $shown | $(if ($null -eq $old) { '-' } else { "$old" }) | $delta |")
        }
        else { [void]$sb.AppendLine("| $($r[0]) | $shown |") }
    }
    [void]$sb.AppendLine()

    $memberLine = { param($x) "- ``$($x['object']).$(if ($x['member']) { $x['member'] } else { $x['kind'] })``$(if ($x['path']) { " ($($x['path']):$($x['line']))" })" }
    $list = {
        param($title, $items, $total)
        if ($total -le 0) { return }
        [void]$sb.AppendLine("### $title ($total)")
        foreach ($x in @($items | Select-Object -First $MaxItems)) { [void]$sb.AppendLine((& $memberLine $x)) }
        if ($total -gt $MaxItems) { [void]$sb.AppendLine("- ... and $($total - $MaxItems) more in the HTML report") }
        [void]$sb.AppendLine()
    }

    if ($imp) {
        [void]$sb.AppendLine("### Impact")
        [void]$sb.AppendLine("$($imp['addedTotal']) added, $($imp['modifiedTotal']) modified, $($imp['removedTotal']) removed; $($imp['affectedTotal']) affected callers in $($imp['affectedObjects']) objects; $($imp['testsTotal']) tests reach the change.")
        [void]$sb.AppendLine()
        & $list 'Modified' $imp['modified'] $imp['modifiedTotal']
        & $list 'Added' $imp['added'] $imp['addedTotal']
        & $list 'Removed' $imp['removed'] $imp['removedTotal']
        & $list 'Affected callers' $imp['affected'] $imp['affectedTotal']
        & $list 'Tests reaching the change' $imp['tests'] $imp['testsTotal']
    }
    if ($f['obsoleteCallsTotal'] -gt 0) {
        [void]$sb.AppendLine("### Obsolete calls ($($f['obsoleteCallsTotal']))")
        foreach ($x in @($f['obsoleteCalls'] | Select-Object -First $MaxItems)) { [void]$sb.AppendLine("- ``$($x['caller'])`` calls ``$($x['callee'])`` ($($x['path']):$($x['line']))") }
        if ($f['obsoleteCallsTotal'] -gt $MaxItems) { [void]$sb.AppendLine("- ... and $($f['obsoleteCallsTotal'] - $MaxItems) more in the HTML report") }
        [void]$sb.AppendLine()
    }
    & $list 'Dead methods' $f['dead'] $f['deadTotal']
    & $list 'Not reached by tests' $f['untested'] $f['untestedTotal']

    $arch = $Analysis['architecture']
    if ($arch) {
        [void]$sb.AppendLine('### Architecture')
        $from = if ($arch['source'] -eq 'file') { 'callgraph.architecture.json' } else { 'the folders (no callgraph.architecture.json yet - `albuild graph describe` writes one)' }
        [void]$sb.AppendLine("$($arch['parts'].Count) parts from $from; $($arch['objects'] - $arch['unassigned']) of $($arch['objects']) objects assigned, $($arch['described']) described, $($arch['flows'].Count) flows.")
        foreach ($w in @($arch['warnings'] | Select-Object -First $MaxItems)) { [void]$sb.AppendLine("- $w") }
        [void]$sb.AppendLine()
    }
    $checks = $Analysis['checks']
    if ($checks) {
        $elevated = @($checks['elevatedOwn']).Count
        $elevatedDep = @($checks['elevatedDependency']).Count
        if ($elevated + $elevatedDep + $checks['nonDebuggableTotal'] -gt 0) {
            [void]$sb.AppendLine('### AL checks (information)')
            if ($elevated) { [void]$sb.AppendLine("- $elevated own object(s) run with inherent permissions or entitlements: " + ((@($checks['elevatedOwn']) | Select-Object -First 5 | ForEach-Object { "``$($_['object'])``" }) -join ', ')) }
            if ($elevatedDep) { [void]$sb.AppendLine("- $elevatedDep Microsoft object(s) with inherent permissions or entitlements are called: " + ((@($checks['elevatedDependency']) | Select-Object -First 5 | ForEach-Object { "``$($_['object'])``" }) -join ', ')) }
            if ($checks['nonDebuggableTotal']) { [void]$sb.AppendLine("- $($checks['nonDebuggableTotal']) product method(s) are [NonDebuggable].") }
            [void]$sb.AppendLine()
        }
    }
    if ($PipelineTab) { [void]$sb.AppendLine('_The interactive graph is in the **Call graph** tab of this build._') }
    return $sb.ToString()
}