Private/Update-RunbookLearning.ps1

function Update-RunbookLearning {
    <#
    .SYNOPSIS
        Records the outcome of a runbook step for the learning system.
    .DESCRIPTION
        Stores success/failure data for each runbook step in the learnings.json file.
        Tracks success rates per step, common failure patterns, and recommended alternatives.
        This data feeds back into future runbook executions to improve decision-making.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$RunbookName,

        [Parameter(Mandatory)]
        [string]$StepId,

        [Parameter(Mandatory)]
        [string]$Action,

        [Parameter(Mandatory)]
        [bool]$Succeeded,

        [Parameter()]
        [string]$ComputerName,

        [Parameter()]
        [hashtable]$Context = @{},

        [Parameter()]
        [string]$LearningsPath
    )

    if (-not $LearningsPath) {
        $LearningsPath = Join-Path $env:USERPROFILE '.runbookengine\learnings.json'
    }

    # Ensure parent directory exists
    $parentDir = Split-Path $LearningsPath -Parent
    if (-not (Test-Path $parentDir)) {
        New-Item -Path $parentDir -ItemType Directory -Force | Out-Null
    }

    # Load existing learnings
    $learnings = @()
    if (Test-Path $LearningsPath) {
        try {
            $raw = Get-Content -Path $LearningsPath -Raw -ErrorAction Stop
            if ($raw -and $raw.Trim().Length -gt 2) {
                $learnings = @($raw | ConvertFrom-Json -ErrorAction Stop)
            }
        }
        catch {
            Write-Verbose "Failed to parse learnings file, starting fresh: $_"
            $learnings = @()
        }
    }

    # Find existing entry for this runbook+step combination
    $existingIdx = -1
    for ($i = 0; $i -lt $learnings.Count; $i++) {
        if ($learnings[$i].RunbookName -eq $RunbookName -and $learnings[$i].StepId -eq $StepId) {
            $existingIdx = $i
            break
        }
    }

    $timestamp = (Get-Date).ToString('o')

    if ($existingIdx -ge 0) {
        # Update existing entry
        $entry = $learnings[$existingIdx]

        $totalRuns = if ($entry.TotalRuns) { [int]$entry.TotalRuns + 1 } else { 1 }
        $successes = if ($entry.Successes) { [int]$entry.Successes } else { 0 }
        $failures = if ($entry.Failures) { [int]$entry.Failures } else { 0 }

        if ($Succeeded) {
            $successes++
        }
        else {
            $failures++
        }

        $successRate = if ($totalRuns -gt 0) { [math]::Round(($successes / $totalRuns) * 100, 1) } else { 0 }

        # Build updated recent executions list
        $recentExecutions = @()
        if ($entry.RecentExecutions) {
            $recentExecutions = @($entry.RecentExecutions)
        }

        $newExecution = [PSCustomObject]@{
            Timestamp    = $timestamp
            ComputerName = $ComputerName
            Succeeded    = $Succeeded
            Context      = $Context
        }

        $recentExecutions = @($recentExecutions) + @($newExecution)

        # Keep only last 50 executions
        if ($recentExecutions.Count -gt 50) {
            $recentExecutions = $recentExecutions | Select-Object -Last 50
        }

        # Detect common failure patterns
        $failurePatterns = @()
        if ($entry.FailurePatterns) {
            $failurePatterns = @($entry.FailurePatterns)
        }

        if (-not $Succeeded -and $Context.ContainsKey('Error')) {
            $errorMsg = $Context['Error']
            $patternFound = $false
            for ($j = 0; $j -lt $failurePatterns.Count; $j++) {
                if ($failurePatterns[$j].Pattern -eq $errorMsg) {
                    $failurePatterns[$j] = [PSCustomObject]@{
                        Pattern   = $errorMsg
                        Count     = ([int]$failurePatterns[$j].Count + 1)
                        LastSeen  = $timestamp
                    }
                    $patternFound = $true
                    break
                }
            }
            if (-not $patternFound) {
                $failurePatterns += [PSCustomObject]@{
                    Pattern  = $errorMsg
                    Count    = 1
                    LastSeen = $timestamp
                }
            }
        }

        $updatedEntry = [PSCustomObject]@{
            RunbookName      = $RunbookName
            StepId           = $StepId
            Action           = $Action
            TotalRuns        = $totalRuns
            Successes        = $successes
            Failures         = $failures
            SuccessRate      = $successRate
            LastRunAt        = $timestamp
            LastResult       = if ($Succeeded) { 'Success' } else { 'Failure' }
            RecentExecutions = $recentExecutions
            FailurePatterns  = $failurePatterns
        }

        $learnings[$existingIdx] = $updatedEntry
    }
    else {
        # Create new entry
        $newEntry = [PSCustomObject]@{
            RunbookName      = $RunbookName
            StepId           = $StepId
            Action           = $Action
            TotalRuns        = 1
            Successes        = if ($Succeeded) { 1 } else { 0 }
            Failures         = if ($Succeeded) { 0 } else { 1 }
            SuccessRate      = if ($Succeeded) { 100.0 } else { 0.0 }
            LastRunAt        = $timestamp
            LastResult       = if ($Succeeded) { 'Success' } else { 'Failure' }
            RecentExecutions = @(
                [PSCustomObject]@{
                    Timestamp    = $timestamp
                    ComputerName = $ComputerName
                    Succeeded    = $Succeeded
                    Context      = $Context
                }
            )
            FailurePatterns  = @()
        }

        if (-not $Succeeded -and $Context.ContainsKey('Error')) {
            $newEntry.FailurePatterns = @(
                [PSCustomObject]@{
                    Pattern  = $Context['Error']
                    Count    = 1
                    LastSeen = $timestamp
                }
            )
        }

        $learnings = @($learnings) + @($newEntry)
    }

    # Save back to file
    $learnings | ConvertTo-Json -Depth 10 | Set-Content -Path $LearningsPath -Encoding UTF8

    # Return the updated/new entry
    if ($existingIdx -ge 0) {
        return $learnings[$existingIdx]
    }
    else {
        return $learnings[-1]
    }
}