Modules/businessdev.ALbuild.RuntimePackages/Private/Get-BcRuntimeIssueClass.ps1

function Get-BcRuntimeIssueClass {
    <#
    .SYNOPSIS
        Reduces one failed (app, platform version) to the CAUSE behind it, so identical causes collapse.
 
    .DESCRIPTION
        A product that cannot compile against older majors fails once per platform version: ERiC produced
        162 identical failures in run 27383, all of them the same line in app.json. Reporting those as 162
        findings is a wall, not a report - and it is a wall the product team has to read before it can
        decide anything.
 
        The cause is the set of AL error codes the compiler produced, which is stable across versions and
        is what a developer recognises: 'AL0280+AL0282' is "we subscribe to events this Base Application
        does not have", 'AL0791' is "we reference namespaces it does not have". The SET, not the first
        code, because the order compiler diagnostics arrive in is not something to build a grouping on.
 
        Failures with no diagnostics at all - a container command that would not run, a signing tool that
        died - are classed by their message with the moving parts taken out: paths, guids, versions and
        bare numbers. Two of those collapse when they are the same problem and stay apart when they are
        not.
 
    .PARAMETER Message
        The failure message recorded for the app. NOT named -Error: '$Error' is a PowerShell automatic
        variable, and a parameter by that name shadows it for the whole function body.
 
    .PARAMETER Diagnostic
        The compiler diagnostics attributed to that app, if any.
 
    .OUTPUTS
        PSCustomObject: Key (stable, for grouping) and Label (for a human).
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [AllowEmptyString()] [AllowNull()] [string] $Message = '',
        [AllowEmptyCollection()] [string[]] $Diagnostic = @()
    )

    $codes = [System.Collections.Generic.SortedSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
    foreach ($d in @($Diagnostic)) {
        foreach ($m in [regex]::Matches("$d", '\berror ([A-Za-z]{2}\d+):')) { [void]$codes.Add($m.Groups[1].Value) }
    }
    if ($codes.Count -gt 0) {
        $key = ($codes -join '+')
        return [PSCustomObject]@{ Key = $key; Label = "compiler errors $key" }
    }

    # No diagnostics: class by the message, with everything that varies per version removed.
    $norm = "$Message"
    $norm = $norm -replace '[A-Za-z]:\\[^\s'']+', '<path>'
    $norm = $norm -replace '[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}', '<guid>'
    $norm = $norm -replace '\b\d+(\.\d+){2,3}\b', '<version>'
    $norm = $norm -replace '\b[0-9a-f]{8}\b', '<id>'
    $norm = $norm -replace '-?\d+', '<n>'
    $norm = ($norm -replace '\s+', ' ').Trim()
    if (-not $norm) { $norm = 'unknown failure' }
    return [PSCustomObject]@{ Key = $norm; Label = $norm }
}