Private/Invoke-KadenManagedInitialize.ps1

function Invoke-KadenManagedInitialize {
    <#
    .SYNOPSIS
        Apply the allowlisted managed payload to a child repository under a transaction journal.
    .DESCRIPTION
        Creates `.kaden/transactions/<id>/` with a lock, stages payload files, and copies only
        manifest destinations that resolve inside the child root. Source paths must resolve
        inside the payload root. On failure, files already copied are restored from backup
        or removed when they were new, the journal status becomes incomplete, and the lock
        is released. Shared `.kaden.json` and gitignored `.kaden.local.json` are written only
        after every managed file verifies.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)][string]$ChildRoot,
        [Parameter(Mandatory = $true)][string]$ProjectId,
        [Parameter(Mandatory = $true)]$Manifest,
        [Parameter(Mandatory = $true)][string]$PayloadRoot,
        [Parameter(Mandatory = $true)][string]$ModuleVersion,
        [Parameter(Mandatory = $false)][switch]$KeepLock
    )

    $operations = @()
    $journal = $null
    $journalPath = $null
    $txnRoot = Join-Path $ChildRoot (Join-Path '.kaden' 'transactions')
    $lockPath = Join-Path $txnRoot 'update.lock'
    New-Item -ItemType Directory -Path $txnRoot -Force | Out-Null

    $transactionId = [guid]::NewGuid().ToString('N')
    Lock-KadenUpdate -LockPath $lockPath -TransactionId $transactionId
    $txnDir = Join-Path $txnRoot $transactionId
    $staging = Join-Path $txnDir 'staging'
    $backup = Join-Path $txnDir 'backup'
    New-Item -ItemType Directory -Path $staging -Force | Out-Null
    New-Item -ItemType Directory -Path $backup -Force | Out-Null
    $releaseLock = $true

    try {
        foreach ($entry in @($Manifest.files)) {
            $destRel = [string]$entry.destination
            if (Test-KadenDelimitedInstructionDestination -Destination $destRel) {
                continue
            }
            $sourceRel = [string]$entry.source
            $expected = [string]$entry.digest
            $sourcePath = Resolve-KadenContainedPath -Root $PayloadRoot -RelativePath $sourceRel
            $stagedPath = Join-Path $staging ($destRel -replace '[\\/]', '__')
            Copy-Item -LiteralPath $sourcePath -Destination $stagedPath -Force

            $operations += [pscustomobject]@{
                destination         = $destRel
                expectedDigest      = $expected
                previousDigest      = $null
                backupPath          = $null
                stagedPath          = $stagedPath
                intentStatus        = 'planned'
                backupStatus        = 'pending'
                replacementStatus   = 'pending'
                verificationStatus  = 'pending'
            }
        }

        $journal = [ordered]@{
            schemaVersion         = 1
            transactionId         = $transactionId
            status                = 'planned'
            startedAtUtc          = [datetime]::UtcNow.ToString('o')
            payloadVersion        = $ModuleVersion
            payloadDigest         = [string]$Manifest.payloadDigest
            managedManifestDigest = Get-KadenFileDigest -LiteralPath (Join-Path $PayloadRoot 'kaden-managed-files.json')
            childRoot             = $ChildRoot
            operations            = $operations
            stateBefore           = $null
            stateAfter            = $null
            verification          = $null
            cleanupEligibleAtUtc  = $null
            failure               = $null
        }
        $journalPath = Join-Path $txnDir 'journal.json'
        Write-KadenJsonAtomic -LiteralPath $journalPath -Object $journal

        for ($i = 0; $i -lt $operations.Count; $i++) {
            $op = $operations[$i]
            $destPath = Resolve-KadenContainedPath -Root $ChildRoot -RelativePath ([string]$op.destination)
            $previousDigest = $null
            $backupPath = $null
            if (Test-Path -LiteralPath $destPath) {
                $previousDigest = Get-KadenFileDigest -LiteralPath $destPath
                $backupPath = Join-Path $backup (($op.destination -replace '[\\/]', '__') + '.bak')
            }
            $operations[$i].previousDigest = $previousDigest
            $operations[$i].backupPath = $backupPath
            $operations[$i].intentStatus = 'backup-intent-recorded'
            $operations[$i].backupStatus = if ($backupPath) { 'intent-recorded' } else { 'not-required' }
            $journal.status = 'backup-incomplete'
            $journal.operations = $operations
            Write-KadenJsonAtomic -LiteralPath $journalPath -Object $journal
            if ($backupPath) {
                Copy-Item -LiteralPath $destPath -Destination $backupPath -Force
                $operations[$i].backupStatus = 'complete'
                $journal.operations = $operations
                Write-KadenJsonAtomic -LiteralPath $journalPath -Object $journal
            }

            $operations[$i].intentStatus = 'replacement-intent-recorded'
            $operations[$i].replacementStatus = 'intent-recorded'
            $journal.status = 'replacement-incomplete'
            $journal.operations = $operations
            Write-KadenJsonAtomic -LiteralPath $journalPath -Object $journal
            $destDir = Split-Path -Parent $destPath
            if (-not (Test-Path -LiteralPath $destDir)) {
                New-Item -ItemType Directory -Path $destDir -Force | Out-Null
            }
            Copy-Item -LiteralPath $op.stagedPath -Destination $destPath -Force
            $operations[$i].replacementStatus = 'complete'
            $journal.operations = $operations
            Write-KadenJsonAtomic -LiteralPath $journalPath -Object $journal
            $actual = Get-KadenFileDigest -LiteralPath $destPath
            if ($actual -ne $op.expectedDigest) {
                throw "KDN-TXN-PREFLIGHT-FAILED: digest mismatch after applying '$($op.destination)'."
            }
            $operations[$i].verificationStatus = 'verified'
            $journal.operations = $operations
            Write-KadenJsonAtomic -LiteralPath $journalPath -Object $journal
        }

        $sharedPath = Join-Path $ChildRoot '.kaden.json'
        $stateBefore = $null
        if (Test-Path -LiteralPath $sharedPath) {
            $priorShared = Get-Content -LiteralPath $sharedPath -Raw | ConvertFrom-Json
            $stateBefore = [ordered]@{
                managedContentVersion = [string]$priorShared.managedContentVersion
                projectId             = [string]$priorShared.projectId
            }
        }
        $journal.stateBefore = $stateBefore
        $journal.status = 'payload-verified'
        $journal.operations = $operations
        $journal.verification = [pscustomobject]@{ status = 'payload-verified'; atUtc = [datetime]::UtcNow.ToString('o') }
        Write-KadenJsonAtomic -LiteralPath $journalPath -Object $journal

        $localPath = Join-Path $ChildRoot '.kaden.local.json'
        $shared = [ordered]@{
            schemaVersion         = 1
            projectId             = $ProjectId
            managedContentVersion = $ModuleVersion
        }
        if (Test-Path -LiteralPath $sharedPath) {
            $existingShared = Get-Content -LiteralPath $sharedPath -Raw | ConvertFrom-Json
            foreach ($prop in @($existingShared.PSObject.Properties)) {
                $name = [string]$prop.Name
                if ($shared.Contains($name)) { continue }
                $shared[$name] = $prop.Value
            }
        }
        $local = [ordered]@{
            schemaVersion      = 1
            lastAppliedVersion = $ModuleVersion
            lastAppliedAtUtc   = [datetime]::UtcNow.ToString('o')
            installationStatus = 'complete'
        }
        Write-KadenJsonAtomic -LiteralPath $sharedPath -Object $shared
        Write-KadenJsonAtomic -LiteralPath $localPath -Object $local

        Add-KadenTransactionalIgnoreEntry -ChildRoot $ChildRoot

        $journal.status = 'completed'
        $journal.stateAfter = [pscustomobject]@{
            managedContentVersion = $ModuleVersion
            projectId             = $ProjectId
        }
        $journal.cleanupEligibleAtUtc = [datetime]::UtcNow.AddDays(30).ToString('o')
        Write-KadenJsonAtomic -LiteralPath $journalPath -Object $journal

        Remove-Item -LiteralPath $staging -Recurse -Force -ErrorAction SilentlyContinue
        Remove-Item -LiteralPath $backup -Recurse -Force -ErrorAction SilentlyContinue
        if ($KeepLock) {
            $releaseLock = $false
        }
    }
    catch {
        $failureMessage = $_.Exception.Message
        try {
            Restore-KadenManagedFile -ChildRoot $ChildRoot -Operations $operations
        }
        catch {
            $failureMessage = "$failureMessage Restore failed: $($_.Exception.Message)"
        }
        foreach ($op in @($operations)) {
            if ([string]$op.replacementStatus -eq 'complete' -or [string]$op.verificationStatus -eq 'verified') {
                $op.replacementStatus = 'pending'
                $op.verificationStatus = 'pending'
            }
        }
        if ($journalPath -and $journal) {
            $journal.status = 'incomplete'
            $journal.operations = $operations
            $journal.failure = [pscustomobject]@{
                atUtc   = [datetime]::UtcNow.ToString('o')
                message = $failureMessage
            }
            Write-KadenJsonAtomic -LiteralPath $journalPath -Object $journal
        }
        throw $failureMessage
    }
    finally {
        if ($releaseLock) {
            Remove-Item -LiteralPath $lockPath -Force -ErrorAction SilentlyContinue
        }
    }
}