Public/Invoke-AvmPrCheck.ps1

function Invoke-AvmPrCheck {
    <#
    .SYNOPSIS
        Run the full pull-request gauntlet against the resolved module:
        format -> transform -> lint -> check policy -> check convention
        -> test -> docs.

    .DESCRIPTION
        Composition cmdlet. Resolves the module context once with
        Get-AvmModuleContext, then invokes the full Phase 1 verb chain in
        sequence against that same module root. Each step's structured
        result is captured. The overall Status is 'pass' only when every
        executed step reports Status='pass' (or didn't throw, for verbs
        that don't carry a Status field).

        This is the broader sibling of Invoke-AvmPreCommit: where
        pre-commit runs the fast, locally meaningful checks (format,
        lint, test, docs), pr-check runs **every check that runs in
        CI** (per plan section 4), including the convention-policy
        steps that compare the module against the AVM specs.

        Status semantics (same as Invoke-AvmPreCommit):
          - 'pass' : step returned Status='pass' (or didn't throw for
                        format).
          - 'fail' : step returned Status='fail'.
          - 'error' : step threw any exception other than
                        AvmConfigurationException; the chain aborts.
          - 'skipped' : step threw AvmConfigurationException - the engine
                        is a deliberate placeholder for a future slice
                        (e.g. bicep-docs, transform, check policy,
                        check convention). The chain CONTINUES and
                        overall status is NOT marked failed by a skip.

        By default the gauntlet is fail-soft: a step that returns
        Status='fail' does NOT abort subsequent steps - the caller gets
        the full picture in one run. A step that THROWS (non-
        AvmConfigurationException) IS fatal and aborts the rest of the
        chain. Set -StopOnFail to abort on the first Status='fail'
        instead.

        Routed by the dispatcher: 'avm pr-check'.

    .PARAMETER Path
        Working directory whose enclosing module to validate. Defaults to
        the current location.

    .PARAMETER Ecosystem
        Force the ecosystem selector. Defaults to 'auto'.

    .PARAMETER AllowPathFallback
        When set, accept a PATH-resolved tool binary that self-reports the
        lock-pinned version. Forwarded to each step.

    .PARAMETER StopOnFail
        When set, abort the chain on the first step whose Status is 'fail'.
        A throwing step is always fatal regardless of this flag.

    .OUTPUTS
        pscustomobject with:
          - Path : the resolved module root
          - Ecosystem : bicep | terraform
          - Status : pass | fail | error
          - Steps : array of { Step, Status, Error?, Result?, DurationMs }
          - DurationMs : total wall-clock cost

    .EXAMPLE
        avm pr-check

    .EXAMPLE
        Invoke-AvmPrCheck -Path C:\repos\my-module -StopOnFail
    #>

    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param(
        [Parameter(Position = 0)]
        [string] $Path = $PWD.Path,

        [ValidateSet('auto', 'bicep', 'terraform')]
        [string] $Ecosystem = 'auto',

        [switch] $AllowPathFallback,

        [switch] $StopOnFail
    )

    Set-StrictMode -Version 3.0
    $ErrorActionPreference = 'Stop'

    $sw = [System.Diagnostics.Stopwatch]::StartNew()

    $context = Get-AvmModuleContext -Path $Path -Ecosystem $Ecosystem

    $stepDefs = @(
        [pscustomobject]@{ Name = 'format'; Cmdlet = 'Invoke-AvmFormat' }
        [pscustomobject]@{ Name = 'transform'; Cmdlet = 'Invoke-AvmTransform'; ExtraArgs = @{ CheckDrift = $true } }
        [pscustomobject]@{ Name = 'lint'; Cmdlet = 'Invoke-AvmLint' }
        [pscustomobject]@{ Name = 'check policy'; Cmdlet = 'Invoke-AvmCheckPolicy' }
        [pscustomobject]@{ Name = 'check convention'; Cmdlet = 'Invoke-AvmCheckConvention' }
        [pscustomobject]@{ Name = 'test'; Cmdlet = 'Invoke-AvmTest' }
        [pscustomobject]@{ Name = 'docs'; Cmdlet = 'Invoke-AvmDocs' }
    )

    $steps = New-Object System.Collections.Generic.List[object]
    $overall = 'pass'

    foreach ($def in $stepDefs) {
        $stepStatus = 'pass'
        $stepError = $null
        $stepResult = $null
        $stepSw = [System.Diagnostics.Stopwatch]::StartNew()

        try {
            $extraArgs = if ($def.PSObject.Properties.Name -contains 'ExtraArgs' -and $def.ExtraArgs) { $def.ExtraArgs } else { @{} }
            $stepResult = & $def.Cmdlet `
                -Path $context.Root `
                -Ecosystem $context.Ecosystem `
                -AllowPathFallback:$AllowPathFallback `
                @extraArgs

            if ($stepResult -and $stepResult.PSObject.Properties.Name -contains 'Status') {
                $stepStatus = $stepResult.Status
            }
        }
        catch [AvmConfigurationException] {
            $stepStatus = 'skipped'
            $stepError = $_.Exception.Message
        }
        catch {
            $stepStatus = 'error'
            $stepError = $_.Exception.Message
        }
        $stepSw.Stop()

        $steps.Add([pscustomobject][ordered]@{
                Step       = $def.Name
                Status     = $stepStatus
                Error      = $stepError
                Result     = $stepResult
                DurationMs = [int]$stepSw.Elapsed.TotalMilliseconds
            })

        if ($stepStatus -eq 'fail' -or $stepStatus -eq 'error') { $overall = $stepStatus }
        if ($stepStatus -eq 'error') { break }
        if ($StopOnFail -and $stepStatus -eq 'fail') { break }
    }

    $sw.Stop()

    return [pscustomobject][ordered]@{
        Path       = $context.Root
        Ecosystem  = $context.Ecosystem
        Status     = $overall
        Steps      = $steps.ToArray()
        DurationMs = [int]$sw.Elapsed.TotalMilliseconds
    }
}