extensions/specrew-speckit/scripts/intake/helpers/Read-IntakeYaml.ps1

<#
.SYNOPSIS
Read intake YAML artifacts without requiring ConvertFrom-Yaml.
 
.DESCRIPTION
Provides narrow parsers for the fixed Feature 049 intake catalogs so the
engine can execute in PowerShell 7 environments that do not ship with the
powershell-yaml module.
#>


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

function Convert-IntakeYamlScalarValue {
    param(
        [AllowNull()]
        [string]$Value
    )

    if ($null -eq $Value) {
        return $null
    }

    $trimmed = $Value.Trim()
    if ([string]::IsNullOrWhiteSpace($trimmed)) {
        return ''
    }

    if (($trimmed.StartsWith('"') -and $trimmed.EndsWith('"')) -or ($trimmed.StartsWith("'") -and $trimmed.EndsWith("'"))) {
        return $trimmed.Substring(1, $trimmed.Length - 2)
    }

    if ($trimmed -eq 'null') {
        return $null
    }

    if ($trimmed -match '^\[(.*)\]$') {
        $inner = $matches[1].Trim()
        if ([string]::IsNullOrWhiteSpace($inner)) {
            return @()
        }

        return @(
            $inner -split ',' |
                ForEach-Object { Convert-IntakeYamlScalarValue -Value $_ } |
                Where-Object { $_ -ne '' }
        )
    }

    if ($trimmed -match '^\d+$') {
        return [int]$trimmed
    }

    if ($trimmed -match '^\d+\.\d+$') {
        return [double]::Parse($trimmed, [System.Globalization.CultureInfo]::InvariantCulture)
    }

    return $trimmed
}

function Convert-IntakeYamlObjectList {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Content,

        [Parameter(Mandatory = $true)]
        [string]$RootKey
    )

    $items = New-Object System.Collections.Generic.List[object]
    $current = $null
    $currentArrayProperty = $null
    $inSection = $false

    foreach ($rawLine in ($Content -split "`r?`n")) {
        if ([string]::IsNullOrWhiteSpace($rawLine)) {
            continue
        }

        if ($rawLine.TrimStart().StartsWith('#')) {
            continue
        }

        if (-not $inSection) {
            if ($rawLine -match ('^{0}:\s*$' -f [regex]::Escape($RootKey))) {
                $inSection = $true
            }
            continue
        }

        if ($rawLine -match '^\s{2}-\s+id:\s*(.+)$') {
            if ($null -ne $current) {
                $items.Add([pscustomobject]$current) | Out-Null
            }

            $current = [ordered]@{
                id = Convert-IntakeYamlScalarValue -Value $matches[1]
            }
            $currentArrayProperty = $null
            continue
        }

        if ($null -eq $current) {
            continue
        }

        if ($rawLine -match '^\s{4}([A-Za-z0-9_]+):\s*(.*)$') {
            $propertyName = $matches[1]
            $propertyValue = $matches[2]

            if ([string]::IsNullOrWhiteSpace($propertyValue)) {
                $current[$propertyName] = @()
                $currentArrayProperty = $propertyName
            }
            else {
                $current[$propertyName] = Convert-IntakeYamlScalarValue -Value $propertyValue
                $currentArrayProperty = $null
            }
            continue
        }

        if ($null -ne $currentArrayProperty -and $rawLine -match '^\s{6}-\s*(.+)$') {
            $current[$currentArrayProperty] += Convert-IntakeYamlScalarValue -Value $matches[1]
        }
    }

    if ($null -ne $current) {
        $items.Add([pscustomobject]$current) | Out-Null
    }

    return $items.ToArray()
}

function Convert-IntakeYamlMapSection {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Content,

        [Parameter(Mandatory = $true)]
        [string]$RootKey
    )

    $map = @{}
    $inSection = $false

    foreach ($rawLine in ($Content -split "`r?`n")) {
        if ([string]::IsNullOrWhiteSpace($rawLine)) {
            continue
        }

        if ($rawLine.TrimStart().StartsWith('#')) {
            continue
        }

        if (-not $inSection) {
            if ($rawLine -match ('^{0}:\s*$' -f [regex]::Escape($RootKey))) {
                $inSection = $true
            }
            continue
        }

        if ($rawLine -match '^[A-Za-z0-9_]+:\s*.*$') {
            break
        }

        if ($rawLine -match '^\s{2}([A-Za-z0-9_]+):\s*(.+)$') {
            $map[$matches[1]] = Convert-IntakeYamlScalarValue -Value $matches[2]
        }
    }

    return $map
}

function Convert-IntakeDepthRules {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Content
    )

    $rules = @{
        mode_a_thresholds = @{}
        mode_b_thresholds = @{
            expertise_range    = @{}
            completeness_range = @{}
        }
        mode_c_thresholds = @{}
        conflict_resolution = @{
            priority_order = @()
        }
    }

    $section = ''
    $subsection = ''

    foreach ($rawLine in ($Content -split "`r?`n")) {
        if ([string]::IsNullOrWhiteSpace($rawLine)) {
            continue
        }

        if ($rawLine.TrimStart().StartsWith('#')) {
            continue
        }

        if ($rawLine -match '^(mode_a_thresholds|mode_b_thresholds|mode_c_thresholds|conflict_resolution):\s*$') {
            $section = $matches[1]
            $subsection = ''
            continue
        }

        if ($rawLine -match '^\s{2}(expertise_range|completeness_range):\s*$') {
            $subsection = $matches[1]
            continue
        }

        if ($rawLine -match '^\s{2}([A-Za-z0-9_]+):\s*(.+)$') {
            $key = $matches[1]
            $value = Convert-IntakeYamlScalarValue -Value $matches[2]
            if ($section -eq 'mode_a_thresholds') {
                $rules.mode_a_thresholds[$key] = $value
            }
            elseif ($section -eq 'mode_c_thresholds') {
                $rules.mode_c_thresholds[$key] = $value
            }
            elseif ($section -eq 'conflict_resolution') {
                $rules.conflict_resolution[$key] = $value
            }
            continue
        }

        if ($rawLine -match '^\s{4}([A-Za-z0-9_]+):\s*(.+)$' -and $section -eq 'mode_b_thresholds' -and -not [string]::IsNullOrWhiteSpace($subsection)) {
            $rules.mode_b_thresholds[$subsection][$matches[1]] = Convert-IntakeYamlScalarValue -Value $matches[2]
        }
    }

    return $rules
}

function Convert-IntakeUserProfile {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Content
    )

    $personaFieldMap = [ordered]@{
        'architect'                     = 'software_architecture'
        'ux-ui-specialist'              = 'ui_ux'
        'product-manager'               = 'product_management'
        'ai-researcher-project-manager' = 'ai_research_project_management'
    }

    $profile = [ordered]@{
        schema                      = '1.0'
        schema_version              = '1.0'
        specrew_version_at_creation = ''
        created_at                  = ''
        last_updated_at             = ''
        updated_at                  = ''
        user_name                   = $null
        expertise                   = [ordered]@{
            software_architecture          = $null
            ui_ux                          = $null
            product_management             = $null
            ai_research_project_management = $null
        }
        preferences                 = [ordered]@{
            preferred_intake_depth = 'auto'
        }
        expertise_dials             = [ordered]@{}
    }

    $section = $null

    foreach ($rawLine in ($Content -split "`r?`n")) {
        if ([string]::IsNullOrWhiteSpace($rawLine)) {
            continue
        }

        if ($rawLine.TrimStart().StartsWith('#')) {
            continue
        }

        if ($rawLine -match '^schema:\s*(.+)$') {
            $profile.schema = Convert-IntakeYamlScalarValue -Value $matches[1]
            continue
        }

        if ($rawLine -match '^schema_version:\s*(.+)$') {
            $profile.schema = Convert-IntakeYamlScalarValue -Value $matches[1]
            continue
        }

        if ($rawLine -match '^(specrew_version_at_creation|created_at|last_updated_at|updated_at|user_name):\s*(.+)$') {
            $profile[$matches[1]] = Convert-IntakeYamlScalarValue -Value $matches[2]
            continue
        }

        if ($rawLine -match '^(expertise|preferences|expertise_dials):\s*$') {
            $section = $matches[1]
            continue
        }

        if ($rawLine -match '^\s{2}([A-Za-z0-9_-]+):\s*(.+)$' -and $null -ne $section) {
            $key = $matches[1]
            $value = Convert-IntakeYamlScalarValue -Value $matches[2]
            if ($section -eq 'preferences') {
                $profile.preferences[$key] = $value
            }
            else {
                $profile[$section][$key] = $value
            }
        }
    }

    foreach ($personaId in $personaFieldMap.Keys) {
        $field = $personaFieldMap[$personaId]
        if ($profile.expertise[$field] -eq 'auto') {
            $profile.expertise[$field] = $null
        }
        elseif ($null -eq $profile.expertise[$field] -and $profile.expertise_dials.Contains($personaId)) {
            $legacyValue = $profile.expertise_dials[$personaId]
            if ($legacyValue -eq 'auto') {
                $profile.expertise[$field] = $null
            }
            elseif ($null -ne $legacyValue) {
                $profile.expertise[$field] = $legacyValue
            }
        }

        $profile.expertise_dials[$personaId] = if ($null -eq $profile.expertise[$field]) { 'auto' } else { $profile.expertise[$field] }
    }

    $profile.schema_version = $profile.schema
    $profile.updated_at = if ($null -ne $profile.last_updated_at -and $profile.last_updated_at -ne '') { $profile.last_updated_at } else { $profile.updated_at }

    return $profile
}

function Read-IntakeYamlDocument {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Path,

        [Parameter(Mandatory = $true)]
        [ValidateSet('personas', 'categories', 'questions', 'defaults', 'depth_rules', 'user_profile')]
        [string]$Kind
    )

    if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
        throw "YAML artifact not found: $Path"
    }

    $content = Get-Content -LiteralPath $Path -Raw -Encoding UTF8

    switch ($Kind) {
        'personas' { return Convert-IntakeYamlObjectList -Content $content -RootKey 'personas' }
        'categories' { return Convert-IntakeYamlObjectList -Content $content -RootKey 'categories' }
        'questions' { return Convert-IntakeYamlObjectList -Content $content -RootKey 'questions' }
        'defaults' { return Convert-IntakeYamlMapSection -Content $content -RootKey 'defaults' }
        'depth_rules' { return Convert-IntakeDepthRules -Content $content }
        'user_profile' { return Convert-IntakeUserProfile -Content $content }
    }
}