Public/Invoke-AICodeReview.ps1

function Invoke-AICodeReview {
    <#
    .SYNOPSIS
    Invoke AI-powered code review using configured model provider
     
    .DESCRIPTION
    Main entry point for AI code review. This function orchestrates the entire review process:
    1. Configures AI provider (github, azure, openai, anthropic)
    2. Gets changed AL files from git
    3. Builds review context with diffs
    4. Loads review rules
    5. Calls appropriate AI provider
    6. Parses and returns violations
     
    This is a model-agnostic function that works with any supported AI provider.
     
    .PARAMETER BaseBranch
    Base branch for git comparison (default: origin/master). Auto-detected in PR builds.
     
    .PARAMETER RulesPath
    Path to directory containing review rules (*.md files).
    Customer can provide rules in .devops/code-review/rules/
     
    .PARAMETER Provider
    AI provider: github, azure, openai, anthropic, or none (to skip review)
     
    .PARAMETER MaxTokens
    Maximum tokens for AI response. If not specified, uses provider default.
     
    .PARAMETER SeverityFailBuild
    Severity level that should cause build failure (error, warning, none). Default: error
     
    .EXAMPLE
    Invoke-AICodeReview -Provider "azure"
    Review changed files using Azure OpenAI
     
    .EXAMPLE
    Invoke-AICodeReview -Provider "github" -SeverityFailBuild "warning"
    Use GitHub Models (free) with stricter failure threshold
     
    .EXAMPLE
    Invoke-AICodeReview -Provider "azure" -RulesPath ".devops/code-review/rules"
    Use customer-specific rules with Azure provider
     
    .OUTPUTS
    System.Object - Review result with Success, Violations, ErrorCount, WarningCount, InfoCount
     
    .NOTES
    Author: waldo
    Version: 1.0.0
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $false)]
        [string]$BaseBranch = "origin/master",
        
        [Parameter(Mandatory = $false)]
        [string]$RulesPath,
        
        [Parameter(Mandatory = $true)]
        [ValidateSet('github', 'azure', 'openai', 'anthropic', 'none')]
        [string]$Provider,
        
        [Parameter(Mandatory = $false)]
        [int]$MaxTokens,
        
        [Parameter(Mandatory = $false)]
        [ValidateSet('error', 'warning', 'none')]
        [string]$SeverityFailBuild = 'error'
    )
    
    begin {
        Write-Verbose "Starting $($MyInvocation.MyCommand.Name)"
        $ErrorActionPreference = 'Stop'
        
        # Load environment variables from .env file if present
        Import-EnvFile
    }
    
    process {
        try {
            # Determine rules path (customer override or module default)
            if (-not $RulesPath) {
                $customerRules = ".devops/code-review/rules"
                if (Test-Path $customerRules) {
                    $RulesPath = $customerRules
                    Write-Verbose "Using customer rules: $RulesPath"
                } else {
                    $RulesPath = Join-Path $PSScriptRoot "../../Rules"
                    Write-Verbose "Using module default rules: $RulesPath"
                }
            }
            
            Write-Host "========================================="
            Write-Host "AI Code Review - Starting"
            Write-Host "========================================="
            
            # Step 1: Get changed files
            Write-Host "`nStep 1: Detecting changed AL files..."
            $changedFiles = Get-ChangedALFiles -BaseBranch $BaseBranch
            
            if ($changedFiles.Count -eq 0) {
                Write-Host "✅ No AL files changed - skipping review"
                return @{
                    Success = $true
                    Violations = @()
                    ErrorCount = 0
                    WarningCount = 0
                    InfoCount = 0
                    Message = "No AL files changed"
                }
            }
            
            # Step 2: Build review context
            Write-Host "`nStep 2: Building review context..."
            $reviewContext = @()
            
            foreach ($file in $changedFiles) {
                $diff = Get-ALFileDiff -FilePath $file -BaseBranch $BaseBranch
                if ($null -ne $diff) {
                    $reviewContext += $diff
                }
            }
            
            if ($reviewContext.Count -eq 0) {
                Write-Host "✅ No substantive changes to review"
                return @{
                    Success = $true
                    Violations = @()
                    ErrorCount = 0
                    WarningCount = 0
                    InfoCount = 0
                    Message = "No substantive changes detected"
                }
            }
            
            Write-Host "✅ Prepared context for $($reviewContext.Count) file(s)"
            
            # Step 3: Load review rules
            Write-Host "`nStep 3: Loading review rules from: $RulesPath"
            $ruleFiles = Get-ChildItem "$RulesPath/*.md" -ErrorAction SilentlyContinue | Where-Object { $_.Name -ne 'system-prompt.md' }
            
            if ($ruleFiles.Count -eq 0) {
                Write-Warning "No rule files found in: $RulesPath"
                Write-Warning "Using basic AL best practices"
                $rules = "Review for AL best practices, code quality, and potential bugs."
            } else {
                $rules = $ruleFiles | ForEach-Object { Get-Content $_.FullName -Raw } | Join-String -Separator "`n`n---`n`n"
                Write-Host "✅ Loaded $($ruleFiles.Count) rule file(s)"
            }
            
            # Load custom system prompt if present
            $systemPromptFile = Join-Path $RulesPath "system-prompt.md"
            $systemPrompt = $null
            if (Test-Path $systemPromptFile) {
                $systemPrompt = Get-Content $systemPromptFile -Raw
                Write-Host "✅ Loaded custom system prompt"
            } else {
                Write-Verbose "No custom system prompt found, using default"
            }
            
            # Step 4: Configure provider
            Write-Host "`nStep 4: Configuring AI provider..."
            
            # Skip AI review if provider is 'none'
            if ($Provider -eq 'none') {
                Write-Host "✅ Provider is 'none' - skipping AI review"
                Write-Host "##[warning]No AI provider configured (provider='none')"
                Write-Host "##[warning]To enable AI code review, configure an API key in the variable group"
                return @{
                    Success = $true
                    Violations = @()
                    ErrorCount = 0
                    WarningCount = 0
                    InfoCount = 0
                    Message = "AI review skipped (no provider configured)"
                }
            }
            
            # Built-in provider configurations
            $providerConfigs = @{
                'github' = @{
                    type = 'openai'
                    api_key_variable = 'GITHUB_TOKEN'
                    endpoint = 'https://models.inference.ai.azure.com/chat/completions'
                    model = 'gpt-4o'
                    max_tokens = 4096
                }
                'azure' = @{
                    type = 'azure'
                    api_key_variable = 'AZURE_AI_API_KEY'
                    endpoint = $env:AZURE_AI_ENDPOINT
                    deployment = $env:AZURE_DEPLOYMENT
                    max_tokens = 4096
                }
                'openai' = @{
                    type = 'openai'
                    api_key_variable = 'OPENAI_API_KEY'
                    endpoint = 'https://api.openai.com/v1/chat/completions'
                    model = 'gpt-4o'
                    max_tokens = 4096
                }
                'anthropic' = @{
                    type = 'anthropic'
                    api_key_variable = 'ANTHROPIC_API_KEY'
                    base_url = 'https://api.anthropic.com/v1'
                    model = 'claude-3-5-sonnet-20241022'
                    max_tokens = 8192
                }
            }
            
            $providerConfig = $providerConfigs[$Provider]
            if (-not $providerConfig) {
                throw "Unknown provider: $Provider. Supported: github, azure, openai, anthropic"
            }
            
            Write-Host "✅ Using provider: $Provider"
            
            # Get API key from environment
            $apiKey = [Environment]::GetEnvironmentVariable($providerConfig.api_key_variable)
            if ([string]::IsNullOrWhiteSpace($apiKey)) {
                throw "API key not found in environment variable: $($providerConfig.api_key_variable)"
            }
            
            # Determine max tokens
            if (-not $MaxTokens) {
                $MaxTokens = $providerConfig.max_tokens
            }
            
            # Step 5: Call AI provider
            Write-Host "`nStep 5: Calling $($providerConfig.type) API..."
            Write-Host " ⏳ This may take 10-60 seconds..."
            
            $violations = switch ($providerConfig.type) {
                "anthropic" {
                    Invoke-AnthropicReview -ReviewContext $reviewContext -Rules $rules -ApiKey $apiKey -Config $providerConfig -MaxTokens $MaxTokens -SystemPromptText $systemPrompt
                }
                "azure" {
                    Invoke-AzureAIReview -ReviewContext $reviewContext -Rules $rules -ApiKey $apiKey -Config $providerConfig -MaxTokens $MaxTokens -SystemPromptText $systemPrompt
                }
                "openai" {
                    Invoke-OpenAIReview -ReviewContext $reviewContext -Rules $rules -ApiKey $apiKey -Config $providerConfig -MaxTokens $MaxTokens -SystemPromptText $systemPrompt
                }
                "github" {
                    Invoke-GitHubModelsReview -ReviewContext $reviewContext -Rules $rules -ApiKey $apiKey -Config $providerConfig -MaxTokens $MaxTokens -SystemPromptText $systemPrompt
                }
                default {
                    throw "Unknown provider type: $($providerConfig.type)"
                }
            }
            
            # Step 6: Summarize results
            Write-Host "`nStep 6: Processing results..."
            
            $errorCount = ($violations | Where-Object { $_.severity -eq 'error' }).Count
            $warningCount = ($violations | Where-Object { $_.severity -eq 'warning' }).Count
            $infoCount = ($violations | Where-Object { $_.severity -eq 'info' }).Count
            
            Write-Host "✅ Review complete: $errorCount errors, $warningCount warnings, $infoCount info"
            
            # Determine success based on severity threshold
            $success = switch ($SeverityFailBuild) {
                'error' { $errorCount -eq 0 }
                'warning' { ($errorCount + $warningCount) -eq 0 }
                'none' { $true }
            }
            
            return @{
                Success = $success
                Violations = $violations
                ErrorCount = $errorCount
                WarningCount = $warningCount
                InfoCount = $infoCount
                SeverityThreshold = $SeverityFailBuild
                Message = if ($success) { "Code review passed" } else { "Code review found violations" }
            }
        }
        catch {
            Write-Error "Error in $($MyInvocation.MyCommand.Name): $_"
            throw
        }
    }
    
    end {
        Write-Verbose "Completed $($MyInvocation.MyCommand.Name)"
    }
}