Private/Parse-AIResponse.ps1

function Parse-AIResponse {
    <#
    .SYNOPSIS
    Parse AI response with multi-tier fallback strategy
     
    .DESCRIPTION
    Attempts to parse AI response as JSON using multiple strategies:
    1. Direct JSON parse
    2. Extract JSON from markdown code blocks
    3. Extract any JSON array pattern
    4. Return empty array on failure
     
    .PARAMETER ResponseText
    Raw text response from AI provider
     
    .EXAMPLE
    $violations = Parse-AIResponse -ResponseText $response
     
    .OUTPUTS
    System.Array - Array of violation objects
     
    .NOTES
    Author: waldo
    Version: 1.0.0
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$ResponseText
    )
    
    begin {
        Write-Verbose "Starting $($MyInvocation.MyCommand.Name)"
    }
    
    process {
        try {
            # Extract PR comment text block from AI response
            $prCommentText = $null
            if ($ResponseText -match '(?s)```pr-comment\s*\n(.*?)\n```') {
                $prCommentText = $matches[1].Trim()
                Write-Verbose "Found PR comment text block ($($prCommentText.Length) chars)"
                
                # Display it in the pipeline output
                Write-Host "`n##[section]AI Review - PR Comment"
                Write-Host $prCommentText
                Write-Host ""
            } else {
                Write-Verbose "No pr-comment block found in AI response"
            }
            
            # Tier 1: Extract violations from code block (preferred format)
            if ($ResponseText -match '(?s)```violations\s*\n(.*?)\n```') {
                try {
                    $json = $matches[1] | ConvertFrom-Json
                    if (Test-ViolationArray -Violations $json) {
                        Write-Verbose "Successfully parsed JSON (violations block)"
                        return @{
                            Violations = $json
                            PRCommentText = $prCommentText
                        }
                    }
                }
                catch {
                    Write-Verbose "Violations block extraction failed: $_"
                }
            }
            
            # Tier 2: Extract JSON from json code blocks (fallback)
            if ($ResponseText -match '(?s)```json\s*\n(.*?)\n```') {
                try {
                    $json = $matches[1] | ConvertFrom-Json
                    if (Test-ViolationArray -Violations $json) {
                        Write-Verbose "Successfully parsed JSON (json block)"
                        return @{
                            Violations = $json
                            PRCommentText = $prCommentText
                        }
                    }
                }
                catch {
                    Write-Verbose "JSON block extraction failed: $_"
                }
            }
            
            # Tier 3: Direct JSON parse (fallback)
            try {
                $json = $ResponseText | ConvertFrom-Json
                if (Test-ViolationArray -Violations $json) {
                    Write-Verbose "Successfully parsed JSON (direct parse)"
                    return @{
                        Violations = $json
                        PRCommentText = $prCommentText
                    }
                }
            }
            catch {
                Write-Verbose "Direct JSON parse failed: $_"
            }
            
            # Tier 4: Try to extract any JSON array (last resort)
            if ($ResponseText -match '(?s)\[\s*\{.*?\}\s*\]') {
                try {
                    $json = $matches[0] | ConvertFrom-Json
                    if (Test-ViolationArray -Violations $json) {
                        Write-Verbose "Successfully parsed JSON (array extraction)"
                        return @{
                            Violations = $json
                            PRCommentText = $prCommentText
                        }
                    }
                }
                catch {
                    Write-Verbose "Array extraction failed: $_"
                }
            }
            
            # Tier 4: Give up gracefully
            Write-Verbose "Could not parse AI response as JSON. Raw response (truncated):"
            Write-Verbose $ResponseText.Substring(0, [Math]::Min(500, $ResponseText.Length))
            
            return @{
                Violations = @()
                PRCommentText = $prCommentText
            }
        }
        catch {
            Write-Error "Error in $($MyInvocation.MyCommand.Name): $_"
            throw
        }
    }
    
    end {
        Write-Verbose "Completed $($MyInvocation.MyCommand.Name)"
    }
}

function Test-ViolationArray {
    <#
    .SYNOPSIS
    Validate that parsed JSON matches expected violation schema
     
    .PARAMETER Violations
    Parsed JSON object to validate
     
    .OUTPUTS
    System.Boolean - True if valid, false otherwise
    #>

    param($Violations)
    
    if ($null -eq $Violations) { return $false }
    
    # Allow empty arrays
    if ($Violations -is [array] -and $Violations.Count -eq 0) { return $true }
    
    # Ensure it's an array
    if ($Violations -isnot [array]) {
        $Violations = @($Violations)
    }
    
    # Validate schema
    foreach ($v in $Violations) {
        if (-not $v.PSObject.Properties['file']) {
            Write-Verbose "Missing 'file' property"
            return $false
        }
        if (-not $v.PSObject.Properties['line']) {
            Write-Verbose "Missing 'line' property"
            return $false
        }
        if (-not $v.PSObject.Properties['severity']) {
            Write-Verbose "Missing 'severity' property"
            return $false
        }
        if (-not $v.PSObject.Properties['message']) {
            Write-Verbose "Missing 'message' property"
            return $false
        }
        
        # Validate severity values
        if ($v.severity -notin @('error', 'warning', 'info')) {
            Write-Verbose "Invalid severity: $($v.severity)"
            return $false
        }
    }
    
    return $true
}