Modules/businessdev.ALbuild.Apps/Private/Get-BcAlProjectRoot.ps1

function Get-BcAlProjectRoot {
    <#
    .SYNOPSIS
        Returns the AL project an .al file belongs to (the nearest ancestor folder holding an app.json).
    .DESCRIPTION
        Used to tell the test side of a repository from the production side: an assertion may only be
        expected in the app that declares the tests, so a call the assertion gate follows has to stay
        inside a project that contains [Test] procedures. Without that boundary the gate would resolve
        any call into production code, where `Error(` is on nearly every page - and pass every test.
    .PARAMETER FilePath
        The .al file.
    .PARAMETER Cache
        Optional dictionary, directory -> project root, so a repository is walked once per folder.
    .OUTPUTS
        The project root path, or '<none>' when no app.json stands above the file.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $FilePath,
        [System.Collections.IDictionary] $Cache
    )

    $dir = Split-Path -Parent $FilePath
    if (-not $dir) { return '<none>' }
    if ($Cache -and $Cache.Contains($dir)) { return [string] $Cache[$dir] }

    $walked = [System.Collections.Generic.List[string]]::new()
    $answer = '<none>'
    $current = $dir
    while ($current) {
        if ($Cache -and $Cache.Contains($current)) { $answer = [string] $Cache[$current]; break }
        $walked.Add($current)
        if (Test-Path -LiteralPath (Join-Path -Path $current -ChildPath 'app.json')) { $answer = $current; break }
        $parent = Split-Path -Parent $current
        if (-not $parent -or $parent -eq $current) { break }
        $current = $parent
    }

    if ($Cache) { foreach ($w in $walked) { $Cache[$w] = $answer } }
    return $answer
}