Private/Resolve-DFRequestedTools.ps1

#Requires -Version 7.0

function Resolve-DFRequestedTools {
    <#
    .SYNOPSIS
        Turns the Tools and ExcludeTools config lists into the session's request set.
    .DESCRIPTION
        Pure: names in, entries out. No tool record is read.
          1. Tools: +group expands to its members; a tool name stays itself
             (matched case-insensitively, returned in its canonical case).
             Each entry records what requested it ('Tools' or '+group'); a
             direct entry wins over a group. Duplicates collapse, first-seen
             order is kept.
          2. ExcludeTools (tools and +groups) mark matching entries Excluded;
             an exclusion always wins. Excluding something that wasn't
             requested warns.
        Unknown tools and groups warn ("did you mean") and are left out; they
        never stop the session.
    .PARAMETER Tools
        The Tools config list.
    .PARAMETER ExcludeTools
        The ExcludeTools config list.
    .PARAMETER GroupDb
        Get-DFGroupDb output.
    .PARAMETER KnownTools
        Every tool name DotForge has a record for (canonical case).
    .PARAMETER Source
        Where the Tools names came from, for warnings. Default: 'Tools'.
    .OUTPUTS
        pscustomobject: Name, RequestedBy, Excluded.
    #>

    [CmdletBinding()]
    param(
        [AllowEmptyCollection()][string[]]$Tools = @(),
        [AllowEmptyCollection()][string[]]$ExcludeTools = @(),
        [Parameter(Mandatory)][System.Collections.IDictionary]$GroupDb,
        [Parameter(Mandatory)][AllowEmptyCollection()][string[]]$KnownTools,
        [string]$Source = 'Tools'
    )

    $canonical = @{}   # case-insensitive name -> canonical name
    foreach ($t in $KnownTools) { $canonical[$t] = $t }
    $groupNames = [string[]]@($GroupDb.Keys)

    # Expands one list entry to canonical tool names, warning about unknowns.
    $expand = {
        param([string]$Entry, [string]$Where)
        $isGroup = $Entry.StartsWith('+')
        if ($isGroup -and -not $GroupDb.Contains($Entry.Substring(1))) {
            $s = Get-DFFieldSuggestion -Name $Entry.Substring(1) -Known $groupNames
            Write-Warning "DotForge: $Where names unknown group '$Entry'$(if ($s) { " — did you mean '+$s'?" }). Run Get-DFToolGroup to list groups."
            return
        }
        foreach ($name in @(Expand-DFGroupEntry -Entry $Entry -GroupDb $GroupDb)) {
            if ($canonical.ContainsKey($name)) { $canonical[$name] }
            elseif (-not $isGroup) {
                $s = Get-DFFieldSuggestion -Name $Entry -Known $KnownTools
                Write-Warning "DotForge: $Where names unknown tool '$Entry'$(if ($s) { " — did you mean '$s'?" })."
            }
        }
    }

    $entries = [ordered]@{}   # canonical name -> entry
    foreach ($item in $Tools) {
        if (-not $item) { continue }
        $by = $item.StartsWith('+') ? $item : 'Tools'
        foreach ($name in @(& $expand $item $Source)) {
            if (-not $entries.Contains($name)) {
                $entries[$name] = [pscustomobject]@{ Name = $name; RequestedBy = $by; Excluded = $false }
            } elseif ($by -eq 'Tools') {
                $entries[$name].RequestedBy = 'Tools'
            }
        }
    }

    foreach ($item in $ExcludeTools) {
        if (-not $item) { continue }
        foreach ($name in @(& $expand $item 'ExcludeTools')) {
            if ($entries.Contains($name)) {
                $entries[$name].Excluded = $true
            } elseif (-not $item.StartsWith('+')) {
                Write-Warning "DotForge: ExcludeTools names '$name', which is not requested in Tools."
            }
        }
    }

    $entries.Values
}

function Expand-DFGroupEntry {
    <#
    .SYNOPSIS
        Expands one Tools/ExcludeTools entry: '+group' to its member names, a tool name to itself.
    .DESCRIPTION
        The one place a +group entry is expanded. An unknown group expands to
        nothing; names are returned as the group lists them, unchecked.
    .PARAMETER Entry
        A tool name or '+group'.
    .PARAMETER GroupDb
        Get-DFGroupDb output.
    .OUTPUTS
        System.String.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory)][string]$Entry,
        [Parameter(Mandatory)][System.Collections.IDictionary]$GroupDb
    )
    if (-not $Entry.StartsWith('+')) { return $Entry }
    $group = $Entry.Substring(1)
    if ($GroupDb.Contains($group)) { $GroupDb[$group].Tools }
}