Public/Get-HDTWizardPage.ps1

function Get-HDTWizardPage {
    <#
        .SYNOPSIS
            Works out which wizard pages this deployment still has to ask.
 
        .DESCRIPTION
            DESIGN 11.2'S SKIP MODEL, MADE REAL. The keys existed and nothing
            read them: HDTSkipWizard appeared only in the MDT name map and in
            error text, and HDTSkipTaskSequence, HDTSkipComputerName and
            HDTSkipSummary appeared nowhere in the engine at all. Only the
            Welcome screen was skippable, from bootstrap.json. An administrator
            could be told exactly what to set, set it, and watch the wizard
            appear anyway.
 
            THE SKIP VARIABLE DECIDES, NOT THE PRESENCE OF A VALUE. That is
            MDT's behaviour - OSDComputerName being set does not hide the page,
            SkipComputerName does - and it matters because a prefilled page a
            technician CONFIRMS is a real workflow. A page that vanished as soon
            as a rule guessed a name would take that away, and a guessed name is
            precisely the one worth confirming: SPIKES S9.11's machine was named
            by a rule nobody checked.
 
            A SKIPPED PAGE WHOSE VALUE IS MISSING IS AN ERROR, NOT A PROMPT.
            DESIGN 11.2, in those words. Showing it anyway produces a deployment
            nobody can reproduce, and inventing a value produces a machine
            nobody named. The refusal names both the variable that should have
            been set and the rule that skipped the page, because an
            administrator who set one and forgot the other cannot tell which
            from a message that names neither.
 
            HDTSkipWizard IS THE BLUNT ONE and is checked the same way, which is
            why it is also the commonest way to get the refusal above: it skips
            pages an administrator may not have realised collected anything.
 
            TRUTH IS READ LOOSELY, ON PURPOSE. rules.yaml gives a real boolean;
            a command line, a machine override or a hand-edited file can give
            'true', 'YES' or '1'. A skip that silently failed to apply because
            it arrived as text is a wizard appearing on a machine nobody is
            standing at.
 
        .PARAMETER Page
            The catalogue - every page this wizard could ask, in order. Each
            carries Id, Title, the Skip variable that hides it, and optionally
            Collect naming the variable it fills.
 
        .PARAMETER Variable
            The resolved variables, from Resolve-HDTVariable.
 
        .INPUTS
            None. This command does not accept pipeline input.
 
        .OUTPUTS
            System.Management.Automation.PSCustomObject with Page (what to ask,
            in order), Skipped (Id and the Rule that did it) and
            IsWizardNeeded.
 
        .EXAMPLE
            $ask = Get-HDTWizardPage -Page $catalogue -Variable $resolved
            if ($ask.IsWizardNeeded) { Show-HDTWizardShell -Page $ask.Page ... }
 
            The whole contract: decide first, and open nothing if there is
            nothing left to ask.
    #>

    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [AllowNull()]
        [AllowEmptyCollection()]
        [object[]] $Page,

        [Parameter()]
        [AllowNull()]
        [hashtable] $Variable
    )

    Set-StrictMode -Version Latest
    $ErrorActionPreference = 'Stop'

    if (@($Page).Count -eq 0) {
        $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Category InvalidArgument `
                    -Message 'there are no wizard pages in the catalogue, so there is nothing to decide about.'))
    }

    $resolved = $Variable
    if ($null -eq $resolved) { $resolved = @{} }

    # See the header: text and booleans both arrive here, and a skip that
    # silently did not apply is a wizard nobody is there to answer.
    $isTrue = {
        param([string] $Name)

        if (-not $resolved.ContainsKey($Name)) { return $false }

        $value = $resolved[$Name]
        if ($null -eq $value) { return $false }
        if ($value -is [bool]) { return [bool] $value }

        return (@('true', 'yes', '1', 'on') -contains ([string] $value).Trim().ToLowerInvariant())
    }

    $skipAll = & $isTrue 'HDTSkipWizard'

    $ask = @()
    $skipped = @()

    foreach ($current in @($Page)) {

        $rule = ''
        if ($null -ne $current.PSObject.Properties['Skip']) { $rule = [string] $current.Skip }

        $isSkipped = $skipAll
        $by = 'HDTSkipWizard'

        if (-not $isSkipped -and -not [string]::IsNullOrWhiteSpace($rule) -and (& $isTrue $rule)) {
            $isSkipped = $true
            $by = $rule
        }

        if (-not $isSkipped) {
            $ask += $current
            continue
        }

        # THE REFUSAL. A page that is not going to be asked must already have
        # the answer it would have collected.
        $collect = $null
        if ($null -ne $current.PSObject.Properties['Collect']) { $collect = $current.Collect }

        # EVERY VARIABLE THE PAGE WOULD HAVE COLLECTED, not just the first. A
        # Computer Details page skipped with a name but no workgroup is the same
        # failure as one skipped with no name at all.
        #
        # A SECRET IS EXEMPT. HDTDomainAdminPassword is deliberately not written
        # into rules.yaml - the summary refuses to print it there - so demanding
        # it from the resolved variables would make the one page that handles a
        # password impossible to skip.
        foreach ($declaration in @($collect)) {
            if ($null -eq $declaration) { continue }

            $isSecret = $false
            if ($null -ne $declaration.PSObject.Properties['IsSecret']) { $isSecret = [bool] $declaration.IsSecret }
            if ($isSecret) { continue }

            # AND A PAGE WITH TWO MUTUALLY EXCLUSIVE HALVES DOES NOT DEMAND
            # BOTH. MDT's Computer Details pane offers a domain OR a workgroup,
            # and SkipDomainMembership needs whichever one the machine is
            # actually getting; demanding every variable meant a workgroup
            # machine could not skip the page without being handed a domain
            # name, an OU and a join account it would never use. The first real
            # zero-touch deployment in this lab failed on exactly that.
            #
            # THE DOCUMENT SAYS WHICH, because only the document knows. Working
            # it out here - "domain things do not matter when a workgroup is
            # set" - would be the engine guessing at the meaning of somebody
            # else's page, and a third-party page would get no such courtesy.
            $isOptional = $false
            if ($null -ne $declaration.PSObject.Properties['Optional']) { $isOptional = [bool] $declaration.Optional }
            if ($isOptional) { continue }

            $name = [string] $declaration.Variable

            $supplied = $false
            if ($resolved.ContainsKey($name)) {
                $supplied = -not [string]::IsNullOrWhiteSpace([string] $resolved[$name])
            }

            if (-not $supplied) {
                $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -ErrorId 'HDTConfigurationError' -Category InvalidData `
                            -Message ('the wizard page ''{0}'' is skipped by {1}, but nothing supplies {2}. A skipped page whose value is still missing is an error rather than a prompt (DESIGN 11.2) - set {2} in rules.yaml, or in Control\machines\<UUID>.yaml for this machine, or stop skipping the page.' -f
                                [string] $current.Id, $by, $name)))
            }
        }

        $skipped += [pscustomobject] @{
            Id    = [string] $current.Id
            Title = [string] $current.Title
            Rule  = $by
        }
    }

    return [pscustomobject] @{
        Page           = $ask
        Skipped        = $skipped
        IsWizardNeeded = (@($ask).Count -gt 0)
    }
}