private/ActionHelpers.ps1

# Shared Action constructor (data-model.md -> Action) so every category file builds the same
# shape instead of repeating a pscustomobject literal eight times.

function New-OctaAction {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][ValidateSet('Registry', 'Service', 'ScheduledTask', 'Package')]
        [string]$TargetType,
        [Parameter(Mandatory)][string]$TargetIdentifier,
        $CurrentValue,
        $PlannedValue,
        [bool]$Reversible = $true,
        [string]$IrreversibleReason = $null,
        [bool]$HeuristicMatch = $false,
        [ValidateSet('Safe', 'Moderate', 'Risky')]
        [string]$RiskLevel = 'Safe'
    )

    if (-not $Reversible -and -not $IrreversibleReason) {
        throw "New-OctaAction: IrreversibleReason is required when Reversible = `$false (FR-007) for target '$TargetIdentifier'."
    }

    return [pscustomobject]@{
        TargetType         = $TargetType
        TargetIdentifier   = $TargetIdentifier
        CurrentValue       = $CurrentValue
        PlannedValue       = $PlannedValue
        Reversible         = $Reversible
        IrreversibleReason = $IrreversibleReason
        HeuristicMatch     = $HeuristicMatch
        RiskLevel          = $RiskLevel
    }
}

function Split-OctaActionsByConfirmationGroup {
    <#
        .SYNOPSIS
        Partitions {Category,Action} entries into four independent confirmation gates
        Invoke-OctaCategory uses: Normal (Safe/Moderate, reversible, non-heuristic),
        Irreversible (Reversible = $false, e.g. appx/onedrive package removal - previously only
        flagged in dry-run text with no dedicated confirmation of its own, found while
        considering adding appx to Quick Clean), Heuristic (HeuristicMatch, e.g. oem-suites),
        and Risky (RiskLevel -eq 'Risky'). Precedence when an action matches more than one:
        Risky > Irreversible > Heuristic > Normal - each action gets exactly one gate, never
        two, same principle already applied to Risky-vs-Heuristic (FR-017, FR-026).
    #>

    [CmdletBinding()]
    param(
        [AllowEmptyCollection()][object[]]$Entries = @()
    )

    $risky = @($Entries | Where-Object { $_.Action.RiskLevel -eq 'Risky' })
    $irreversible = @($Entries | Where-Object { -not $_.Action.Reversible -and $_.Action.RiskLevel -ne 'Risky' })
    $heuristic = @($Entries | Where-Object { $_.Action.HeuristicMatch -and $_.Action.Reversible -and $_.Action.RiskLevel -ne 'Risky' })
    $normal = @($Entries | Where-Object { -not $_.Action.HeuristicMatch -and $_.Action.Reversible -and $_.Action.RiskLevel -ne 'Risky' })

    return [pscustomobject]@{ Normal = $normal; Heuristic = $heuristic; Irreversible = $irreversible; Risky = $risky }
}