Private/Assert-RaidinessIntakeAnswer.ps1

<#
    One answer, checked against its question.

    Refusing here rather than at evaluation time is the difference between "the
    intake says maybe and the check quietly treated it as no" and a message
    naming the question and its allowed values. A wrong answer is worse than a
    missing one: missing degrades honestly to "not measured".
#>

function Assert-RaidinessIntakeAnswer {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [object] $Question,

        [Parameter(Mandatory = $true)]
        [object] $Value
    )

    $text = "$Value".Trim()

    switch ($Question.Type) {
        'boolean' {
            if ($Value -is [bool]) { return $Value }
            if ($text -in 'true', 'yes', 'y', 'ja', '1') { return $true }
            if ($text -in 'false', 'no', 'n', 'nee', '0') { return $false }
            throw "'$($Question.Key)' is a yes/no question; '$text' is neither. Answer yes or no, or leave it empty."
        }
        'number' {
            $parsed = 0.0
            if ([double]::TryParse($text.Replace(',', '.'), [Globalization.NumberStyles]::Float, [Globalization.CultureInfo]::InvariantCulture, [ref] $parsed)) {
                return $parsed
            }
            throw "'$($Question.Key)' expects a number; '$text' is not one."
        }
        'choice' {
            if ($Question.Options.Count -eq 0) { return $text }
            $match = $Question.Options | Where-Object { $_ -eq $text } | Select-Object -First 1
            if ($match) { return $match }
            # A case difference is a typo, not a different answer.
            $loose = $Question.Options | Where-Object { $_ -ieq $text } | Select-Object -First 1
            if ($loose) { return $loose }
            throw "'$($Question.Key)' accepts $($Question.Options -join ', '); '$text' is none of those."
        }
        default { return $text }
    }
}