Private/Get-StructuredErrorFromStderr.ps1
|
# Copyright (c) 2026 Jeffrey Snover. All rights reserved. # Licensed under the MIT License. See LICENSE file in the project root. function Get-StructuredErrorFromStderr { <# .SYNOPSIS Scan a stderr buffer for an ActionableError JSON line and return its fields. .DESCRIPTION The TypeScript debate engine (and other subprocesses) may emit a single-line JSON object on stderr that conforms to the ActionableError shape: {"goal":"...", "problem":"...", "location":"...", "next_steps":["...","..."]} This helper scans the buffer newest-line-first and returns a normalized PSCustomObject when it finds such a line. Returns $null when nothing matches, so callers can fall back to their existing generic error path. Required keys (all four must be present for the line to qualify): goal, problem, location, next_steps Lines that ConvertFrom-Json fails on are silently skipped (most stderr is plain text and would throw). .PARAMETER StderrLines The captured stderr buffer (array of strings, one per line). May be empty, null, or contain empty strings — the function handles all gracefully. .OUTPUTS PSCustomObject with Goal/Problem/Location/NextSteps (array) and RawLine on hit, $null on miss. .EXAMPLE $err = Get-StructuredErrorFromStderr -StderrLines $StdErr if ($err) { throw "[ActionableError] $($err.Goal): $($err.Problem)" } .EXAMPLE # Scan a captured subprocess stderr buffer $stderr = @('starting up', '{"goal":"G","problem":"P","location":"L","next_steps":["x"]}') Get-StructuredErrorFromStderr -StderrLines $stderr #> [CmdletBinding()] [OutputType([PSCustomObject])] param( [Parameter(Mandatory)] [AllowEmptyCollection()] [AllowEmptyString()] [AllowNull()] [string[]]$StderrLines ) Set-StrictMode -Version Latest # Scan newest line first — the structured error is typically the last # meaningful thing the engine writes before exiting. $reversed = @($StderrLines | Where-Object { $_ -and $_.Trim() }) [Array]::Reverse($reversed) foreach ($line in $reversed) { $trimmed = $line.Trim() # Cheap pre-filter: must look like a single-line JSON object if (-not $trimmed.StartsWith('{') -or -not $trimmed.EndsWith('}')) { continue } try { $obj = $trimmed | ConvertFrom-Json -ErrorAction Stop } catch { # Not JSON — fall through to the next line continue } # Require all four ActionableError fields as a shape gate. PSObject.Properties # guarded per the project's strict-mode + ConvertFrom-Json convention. $hasGoal = $obj.PSObject.Properties['goal'] $hasProblem = $obj.PSObject.Properties['problem'] $hasLocation = $obj.PSObject.Properties['location'] $hasNextSteps = $obj.PSObject.Properties['next_steps'] if (-not ($hasGoal -and $hasProblem -and $hasLocation -and $hasNextSteps)) { continue } # Normalize next_steps to a string[] — TS may emit a single string or an array. $steps = @() if ($obj.next_steps -is [System.Collections.IEnumerable] -and -not ($obj.next_steps -is [string])) { foreach ($s in $obj.next_steps) { $steps += [string]$s } } else { $steps = @([string]$obj.next_steps) } return [PSCustomObject]@{ Goal = [string]$obj.goal Problem = [string]$obj.problem Location = [string]$obj.location NextSteps = $steps RawLine = $trimmed } } return $null } |