Modules/businessdev.ALbuild.Apps/Private/ConvertFrom-BcCompilerOutput.ps1

function ConvertFrom-BcCompilerOutput {
    <#
    .SYNOPSIS
        Parses AL compiler (alc) output into structured diagnostics.
    .DESCRIPTION
        Internal, pure helper (no I/O) so it is unit-testable. AL diagnostics follow the MSBuild
        convention:
            <file>(<line>,<col>): <severity> <code>: <message>
        where severity is error | warning | info and code is like AL0118 / AA0137 / AW0006. Some
        diagnostics have no source location (<severity> <code>: <message>). Returns one object per
        matched diagnostic (Severity, Code, File, Line, Column, Message); non-diagnostic lines
        (progress, summary counts, ...) are ignored.
    .PARAMETER Output
        The raw compiler output (stdout+stderr) as a single string.
    .OUTPUTS
        PSCustomObject[]: Severity, Code, File, Line, Column, Message.
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject[]])]
    param([string] $Output)

    $result = [System.Collections.Generic.List[PSCustomObject]]::new()
    if ([string]::IsNullOrEmpty($Output)) { return $result.ToArray() }

    # 'file(line,col): sev CODE: message' and the location-less 'sev CODE: message'.
    $withLoc = [regex] '^(?<file>.+?)\((?<line>\d+),(?<col>\d+)\):\s*(?<sev>error|warning|info)\s+(?<code>[A-Za-z]{2}\d+):\s*(?<msg>.*)$'
    $noLoc = [regex] '^\s*(?<sev>error|warning|info)\s+(?<code>[A-Za-z]{2}\d+):\s*(?<msg>.*)$'

    foreach ($line in ($Output -split "`r?`n")) {
        $t = $line.TrimEnd()
        if (-not $t) { continue }
        $m = $withLoc.Match($t)
        if ($m.Success) {
            $result.Add([PSCustomObject]@{
                    Severity = $m.Groups['sev'].Value.ToLowerInvariant()
                    Code     = $m.Groups['code'].Value
                    File     = $m.Groups['file'].Value.Trim()
                    Line     = [int] $m.Groups['line'].Value
                    Column   = [int] $m.Groups['col'].Value
                    Message  = $m.Groups['msg'].Value.Trim()
                })
            continue
        }
        $m = $noLoc.Match($t)
        if ($m.Success) {
            $result.Add([PSCustomObject]@{
                    Severity = $m.Groups['sev'].Value.ToLowerInvariant()
                    Code     = $m.Groups['code'].Value
                    File     = ''
                    Line     = 0
                    Column   = 0
                    Message  = $m.Groups['msg'].Value.Trim()
                })
        }
    }
    return $result.ToArray()
}