public/Get-OctaCategory.ps1

# Registry of all category metadata functions (data-model.md -> Category). Each entry names
# the function (defined in categories/*.ps1) that returns that category's metadata object.
$script:OctaCategoryFunctions = @(
    'Get-OctaTelemetryCategory',
    'Get-OctaAppxCategory',
    'Get-OctaOneDriveCategory',
    'Get-OctaCopilotCategory',
    'Get-OctaWidgetsCategory',
    'Get-OctaRecallCategory',
    'Get-OctaExplorerStartCategory',
    'Get-OctaOemSuitesCategory',
    'Get-OctaPerformanceCategory',
    'Get-OctaPowerCategory',
    'Get-OctaUserExperienceCategory',
    'Get-OctaGpuCategory',
    'Get-OctaServicesCategory',
    'Get-OctaDesktopCategory',
    'Get-OctaPreferencesCategory',
    'Get-OctaGamingCategory',
    'Get-OctaAiFeaturesCategory',
    'Get-OctaWindowsFeaturesCategory',
    'Get-OctaBackgroundActivityCategory',
    'Get-OctaRegistryOrphansCategory',
    'Get-OctaNetworkCategory'
)

$script:OctaRiskRank = @{ Safe = 0; Moderate = 1; Risky = 2 }

function Get-OctaCategory {
    <#
        .SYNOPSIS
        Lists all Octa categories, or a single one by Id.

        HighestRiskLevel is computed here from a live scan (GetActionsFunction), not stored
        statically on the category, so it always reflects current machine state - FR-027.

        .PARAMETER SkipRiskScan
        Skips the live GetActionsFunction scan entirely and reports HighestRiskLevel as
        'Unknown' - for callers that only need cheap metadata (Id/DisplayName/Description),
        not a real risk badge. Found needed for real: Invoke-Octa's interactive menu called
        plain Get-OctaCategory (all 20, no -Id) just to build its group/category picker labels,
        which never displays HighestRiskLevel at all - that alone took ~20s to launch the menu,
        one full live scan per category for a value nothing was reading.
    #>

    [CmdletBinding()]
    param(
        [string]$Id,
        [switch]$SkipRiskScan
    )

    # ponytail: when -Id is given, only resolve/scan the requested category - not every
    # registered one. Metadata functions are cheap (a static object literal), so calling them
    # all just to filter by Id costs nothing; the expensive part is GetActionsFunction, which
    # this skips for every non-matching category. Found because Invoke-OctaCategory calls this
    # once per bundle member - Quick Clean (7 categories after 006's appx/oem-suites addition)
    # was doing 7 x 18 full scans instead of 7, taking minutes instead of seconds.
    $functionNames = $script:OctaCategoryFunctions
    if ($Id) {
        $functionNames = $script:OctaCategoryFunctions | Where-Object { (& $_).Id -eq $Id }
    }

    $categories = $functionNames | ForEach-Object {
        $category = & $_
        if ($SkipRiskScan) {
            Add-Member -InputObject $category -MemberType NoteProperty -Name 'HighestRiskLevel' -Value 'Unknown' -Force
            return $category
        }
        $highest = 'Safe'
        try {
            # A category's scan can legitimately require elevation (e.g. Appx's
            # Get-AppxPackage -AllUsers) - Get-OctaCategory has no elevation gate of its own
            # (that only happens in Invoke-OctaCategory before an actual apply/dry-run), so a
            # scan failure here must not crash a plain `--list` for a non-elevated user.
            $actions = @(& $category.GetActionsFunction)
            foreach ($action in $actions) {
                if ($script:OctaRiskRank[$action.RiskLevel] -gt $script:OctaRiskRank[$highest]) {
                    $highest = $action.RiskLevel
                }
            }
        }
        catch {
            $highest = 'Unknown'
        }
        Add-Member -InputObject $category -MemberType NoteProperty -Name 'HighestRiskLevel' -Value $highest -Force
        $category
    }

    return $categories
}