Modules/businessdev.ALbuild.Core/Public/Repair-ALbuildModule.ps1

function Repair-ALbuildModule {
    <#
    .SYNOPSIS
        Make a module root healthy: reinstall the target version if it is broken, park any other broken
        versions for post-mortem, and prune old ones.
 
    .DESCRIPTION
        The recovery half of the integrity design (§4.4/§4.5). Given a target version:
          - (re)install it atomically if it is missing or fails verification (Install-ALbuildModuleVersion
            re-checks and only rewrites a broken/absent folder);
          - park every OTHER version that fails Assert-ALbuildModuleComplete as '<v>.defekt-<timestamp>'
            (renamed, never deleted - R8), so a poisoned side-by-side copy can't be imported and is kept
            for analysis;
          - prune healthy old versions to -KeepVersions.
 
    .PARAMETER Version
        The version to guarantee is present and complete (e.g. '2.18.2').
 
    .PARAMETER ModuleRoot
        Module root to repair. Default: this edition's CurrentUser root.
 
    .PARAMETER Repository
        Repository for the reinstall. Default 'PSGallery'.
 
    .PARAMETER KeepVersions
        Newest versions to retain when pruning. Default 3.
 
    .OUTPUTS
        PSCustomObject { Version; Path; Installed; Parked [string[]]; Pruned [string[]] }.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $Version,
        [string] $ModuleRoot,
        [string] $Repository = 'PSGallery',
        [ValidateRange(1, [int]::MaxValue)] [int] $KeepVersions = 3
    )

    if (-not $ModuleRoot) { $ModuleRoot = Get-ALbuildUserModuleRoot }
    $moduleDir = Join-Path $ModuleRoot 'businessdev.ALbuild'

    # Park OTHER broken versions first (so a poisoned copy can't be picked up), skipping the target itself
    # - Install below owns the target's repair. Renamed, never deleted (R8).
    $parked = New-Object System.Collections.Generic.List[string]
    if (Test-Path -LiteralPath $moduleDir) {
        foreach ($dir in @(Get-ChildItem -LiteralPath $moduleDir -Directory -ErrorAction SilentlyContinue)) {
            $v = $null
            if (-not [version]::TryParse($dir.Name, [ref]$v)) { continue }       # skips '*.defekt-*' + staging
            if ($dir.Name -eq $Version) { continue }
            if ((Assert-ALbuildModuleComplete -Path $dir.FullName -PassThru).Complete) { continue }
            $defekt = "$($dir.FullName).defekt-$((Get-Date).ToString('yyyyMMddHHmmss'))"
            if ($PSCmdlet.ShouldProcess($dir.FullName, 'Park broken businessdev.ALbuild version')) {
                try { [System.IO.Directory]::Move($dir.FullName, $defekt); $parked.Add($dir.Name); Write-ALbuildLog -Level Warning "Parked broken businessdev.ALbuild $($dir.Name) as '$defekt'." }
                catch { Write-ALbuildLog -Level Warning "Could not park '$($dir.FullName)': $($_.Exception.Message)" }
            }
        }
    }

    # Ensure the target is present + complete (atomic; prunes inside its own lock).
    $install = Install-ALbuildModuleVersion -Version $Version -ModuleRoot $ModuleRoot -Repository $Repository -KeepVersions $KeepVersions

    [PSCustomObject]@{
        Version   = $Version
        Path      = if ($install) { $install.Path } else { Join-Path $moduleDir $Version }
        Installed = if ($install) { $install.Action -eq 'installed' } else { $false }
        Parked    = $parked.ToArray()
        Pruned    = @()
    }
}