Public/Set-RaidinessIntake.ps1

# psrunner-lint allow: New-Item — creates the local data directory the answers are stored in; never a tenant object
function Set-RaidinessIntake {
    <#
    .SYNOPSIS
        Stores intake answers with a run, so the checks that need them can be
        answered instead of reported as "not measured".

    .DESCRIPTION
        Takes a filled-in questionnaire (JSON or CSV from Get-RaidinessIntake,
        or a plain {key: answer} object), checks the answers against the
        questions, and writes them into the run's data folder. The assessment
        picks the file up like any other evidence, so the next
        Export-RaidinessReport already reflects it.

        -Interactive walks the questions in the console instead: Enter keeps
        what is there, "?" shows the explanation, "s" skips the rest of the
        section. Answering nothing is always allowed -- an unanswered question
        leaves its check "not measured", which is the honest result.

        An answer that does not fit its question is refused, with the allowed
        values named. A key the questionnaire does not know is a warning, not
        an error, so an older file still loads against a newer module.

    .PARAMETER RunPath
        The run folder to store the answers in (from Invoke-Raidiness -Path).

    .PARAMETER Path
        Store the answers in this data folder instead of a run folder.

    .PARAMETER FromFile
        The filled-in questionnaire to read.

    .PARAMETER Answer
        Answers as a hashtable, merged over anything read from -FromFile.

    .PARAMETER Interactive
        Ask the questions in the console.

    .PARAMETER Section
        Only these sections, for -Interactive.

    .PARAMETER PassThru
        Return the stored answers.

    .EXAMPLE
        Set-RaidinessIntake -RunPath ./raidiness/2026-09-02-1431 -Interactive

    .EXAMPLE
        Set-RaidinessIntake -RunPath ./raidiness/2026-09-02-1431 -Answer @{
            'choice.dataResidency' = 'eudb'
            'pilot.size' = 25
        }
    #>

    [CmdletBinding(DefaultParameterSetName = 'RunFolder', SupportsShouldProcess = $true)]
    [OutputType([pscustomobject])]
    param(
        [Parameter(ParameterSetName = 'RunFolder', Mandatory = $true)]
        [string] $RunPath,

        [Parameter(ParameterSetName = 'DataFolder', Mandatory = $true)]
        [string] $Path,

        [string] $FromFile,
        [hashtable] $Answer,
        [switch] $Interactive,
        [string[]] $Section,
        [switch] $PassThru
    )

    $ErrorActionPreference = 'Stop'

    $dataPath = if ($PSCmdlet.ParameterSetName -eq 'RunFolder') {
        if (-not (Test-Path -Path $RunPath)) { throw "No run folder at $RunPath." }
        Join-Path (Resolve-Path -Path $RunPath).Path 'data'
    }
    else { $Path }

    if (-not $FromFile -and -not $Answer -and -not $Interactive) {
        throw 'Give -FromFile, -Answer or -Interactive; there is nothing to store otherwise.'
    }

    $catalog = Read-RaidinessIntakeCatalog
    $byKey = @{}
    foreach ($question in $catalog.Questions) {
        $byKey[$question.Key] = $question
        foreach ($alias in $question.Aliases) { $byKey[$alias] = $question }
    }

    # Whatever is already stored is the starting point, so a second run adds to
    # the answers rather than replacing them.
    $answers = @{}
    $stored = Join-Path $dataPath 'raidiness-intake.json'
    if (Test-Path -Path $stored) {
        foreach ($pair in (Read-RaidinessIntakeAnswer -Path $stored).GetEnumerator()) { $answers[$pair.Key] = $pair.Value }
    }
    if ($FromFile) {
        foreach ($pair in (Read-RaidinessIntakeAnswer -Path $FromFile).GetEnumerator()) { $answers[$pair.Key] = $pair.Value }
    }
    if ($Answer) {
        foreach ($key in $Answer.Keys) { $answers[$key] = $Answer[$key] }
    }

    if ($Interactive) {
        $questions = $Section ? @($catalog.Questions | Where-Object { $_.Section -in $Section }) : $catalog.Questions
        $answers = Read-RaidinessIntakeInteractive -Questions $questions -Answers $answers
    }

    # Validate before writing: a refused answer must not reach the engine, and
    # an unknown key must not silently disappear either.
    $clean = [ordered]@{}
    foreach ($key in ($answers.Keys | Sort-Object)) {
        $value = $answers[$key]
        if ($null -eq $value -or "$value".Trim() -eq '') { continue }

        $question = $byKey[$key]
        if (-not $question) {
            Write-Warning "Unknown intake key '$key' — stored, but no check reads it. A typo, or a newer questionnaire?"
            $clean[$key] = $value
            continue
        }

        $clean[$question.Key] = Assert-RaidinessIntakeAnswer -Question $question -Value $value
    }

    if (-not $PSCmdlet.ShouldProcess($stored, "store $($clean.Count) intake answer(s)")) {
        if ($PassThru) { return [pscustomobject] $clean }
        return
    }

    New-Item -ItemType Directory -Path $dataPath -Force | Out-Null
    $envelope = [ordered]@{
        raidinessIntake = [ordered]@{
            version     = '1.0'
            generatedAt = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ')
        }
        answers         = $clean
    }
    ConvertTo-Json -InputObject $envelope -Depth 8 | Out-File -FilePath $stored -Encoding utf8

    $total = $catalog.Questions.Count
    Write-Host "Intake stored: $($clean.Count) of $total question(s) answered — $stored" -ForegroundColor Green
    $personal = @($catalog.Questions | Where-Object { $_.Personal -and $clean.Contains($_.Key) })
    if ($personal.Count -gt 0) {
        Write-Host " $($personal.Count) answer(s) name a person; those are hashed in the LLM bundle unless -IncludeIdentities." -ForegroundColor Cyan
    }
    Write-RaidinessLog -Phase 'intake' -Message "$($clean.Count) of $total answered"

    if ($PassThru) { [pscustomobject] $clean }
}