< Summary

Information
Class: Orchestrator.Commands.Observability.Cost.CostCommand
Assembly: Orchestrator
File(s): /home/runner/work/KicktippAi/KicktippAi/src/Orchestrator/Commands/Observability/Cost/CostCommand.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 351
Coverable lines: 351
Total lines: 585
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 276
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
ExecuteAsync()0%109201040%
ParseMatchdays(...)0%7280%
GetAvailableMatchdays()0%7280%
ParseModels(...)0%110100%
ParseCommunityContexts(...)0%110100%
GetAvailableModels()0%156120%
GetAvailableCommunityContexts()0%156120%
LoadConfigurationFromFile()0%4260%
MergeConfigurations(...)0%1640400%

File(s)

/home/runner/work/KicktippAi/KicktippAi/src/Orchestrator/Commands/Observability/Cost/CostCommand.cs

#LineLine coverage
 1using Microsoft.Extensions.Logging;
 2using Spectre.Console.Cli;
 3using Spectre.Console;
 4using Google.Cloud.Firestore;
 5using System.Globalization;
 6using System.Text.Json;
 7using EHonda.KicktippAi.Core;
 8using Orchestrator.Infrastructure.Factories;
 9
 10namespace Orchestrator.Commands.Observability.Cost;
 11
 12public class CostCommand : AsyncCommand<CostSettings>
 13{
 14    private readonly IAnsiConsole _console;
 15    private readonly IFirebaseServiceFactory _firebaseServiceFactory;
 16    private readonly ILogger<CostCommand> _logger;
 17
 018    public CostCommand(
 019        IAnsiConsole console,
 020        IFirebaseServiceFactory firebaseServiceFactory,
 021        ILogger<CostCommand> logger)
 22    {
 023        _console = console;
 024        _firebaseServiceFactory = firebaseServiceFactory;
 025        _logger = logger;
 026    }
 27
 28    public override async Task<int> ExecuteAsync(CommandContext context, CostSettings settings)
 29    {
 30
 31        try
 32        {
 33            // Load configuration from file if specified
 034            if (!string.IsNullOrWhiteSpace(settings.ConfigFile))
 35            {
 036                var fileConfig = await LoadConfigurationFromFile(settings.ConfigFile);
 037                settings = MergeConfigurations(fileConfig, settings);
 38            }
 39
 40            // Create Firebase services using factory (factory handles env var loading)
 041            var firestoreDb = _firebaseServiceFactory.FirestoreDb;
 042            var predictionRepository = _firebaseServiceFactory.CreatePredictionRepository();
 43
 044            _console.MarkupLine($"[green]Cost command initialized[/]");
 45
 046            if (settings.Verbose)
 47            {
 048                _console.MarkupLine("[dim]Verbose mode enabled[/]");
 49            }
 50
 051            if (settings.All)
 52            {
 053                _console.MarkupLine("[blue]All mode enabled - aggregating over all available data[/]");
 54            }
 55
 56            // Parse filter parameters
 057            var matchdays = ParseMatchdays(settings);
 058            var models = ParseModels(settings);
 059            var communityContexts = ParseCommunityContexts(settings);
 60
 61            // Get available models and community contexts if not specified
 062            var availableModels = models ?? await GetAvailableModels(firestoreDb);
 063            var availableCommunityContexts = communityContexts ?? await GetAvailableCommunityContexts(firestoreDb);
 064            var availableMatchdays = matchdays ?? await GetAvailableMatchdays(firestoreDb);
 65
 066            if (settings.Verbose)
 67            {
 068                _console.MarkupLine($"[dim]Filters:[/]");
 069                _console.MarkupLine($"[dim]  Matchdays: {(matchdays?.Any() == true ? string.Join(", ", matchdays) : $"al
 070                _console.MarkupLine($"[dim]  Models: {(models?.Any() == true ? string.Join(", ", models) : $"all ({avail
 071                _console.MarkupLine($"[dim]  Community Contexts: {(communityContexts?.Any() == true ? string.Join(", ", 
 072                _console.MarkupLine($"[dim]  Include Bonus: {settings.Bonus || settings.All}[/]");
 73            }
 74
 75            // Calculate costs
 076            var totalCost = 0.0;
 077            var matchPredictionCost = 0.0;
 078            var bonusPredictionCost = 0.0;
 079            var matchPredictionCount = 0;
 080            var bonusPredictionCount = 0;
 81
 82            // Structure to store detailed breakdown data with reprediction index support
 083            var detailedData = new List<(string CommunityContext, string Model, string Category, int RepredictionIndex, 
 84
 085            _console.Status()
 086                .Spinner(Spinner.Known.Dots)
 087                .Start("Calculating costs...", ctx =>
 088                {
 089                    foreach (var model in availableModels)
 090                    {
 091                        foreach (var communityContext in availableCommunityContexts)
 092                        {
 093                            ctx.Status($"Processing {model} - {communityContext}...");
 094
 095                            if (settings.Verbose)
 096                            {
 097                                _console.MarkupLine($"[dim]  Processing model: {model}, community context: {communityCon
 098                            }
 099
 0100                            // Get match prediction costs by reprediction index
 0101                            var matchCostsByIndex = predictionRepository.GetMatchPredictionCostsByRepredictionIndexAsync
 0102
 0103                            foreach (var kvp in matchCostsByIndex)
 0104                            {
 0105                                var repredictionIndex = kvp.Key;
 0106                                var (cost, count) = kvp.Value;
 0107
 0108                                matchPredictionCost += cost;
 0109                                matchPredictionCount += count;
 0110
 0111                                // Store detailed data for breakdown
 0112                                if (settings.DetailedBreakdown && (cost > 0 || count > 0))
 0113                                {
 0114                                    detailedData.Add((communityContext, model, "Match", repredictionIndex, count, cost))
 0115                                }
 0116
 0117                                if (settings.Verbose && (cost > 0 || count > 0))
 0118                                {
 0119                                    _console.MarkupLine($"[dim]    Match predictions (reprediction {repredictionIndex}):
 0120                                }
 0121                            }
 0122
 0123                            // Get bonus prediction costs if requested or if all mode is enabled
 0124                            if (settings.Bonus || settings.All)
 0125                            {
 0126                                var bonusCostsByIndex = predictionRepository.GetBonusPredictionCostsByRepredictionIndexA
 0127
 0128                                foreach (var kvp in bonusCostsByIndex)
 0129                                {
 0130                                    var repredictionIndex = kvp.Key;
 0131                                    var (cost, count) = kvp.Value;
 0132
 0133                                    bonusPredictionCost += cost;
 0134                                    bonusPredictionCount += count;
 0135
 0136                                    // Store detailed data for breakdown
 0137                                    if (settings.DetailedBreakdown && (cost > 0 || count > 0))
 0138                                    {
 0139                                        detailedData.Add((communityContext, model, "Bonus", repredictionIndex, count, co
 0140                                    }
 0141
 0142                                    if (settings.Verbose && (cost > 0 || count > 0))
 0143                                    {
 0144                                        _console.MarkupLine($"[dim]    Bonus predictions (reprediction {repredictionInde
 0145                                    }
 0146                                }
 0147                            }
 0148                        }
 0149                    }
 0150                });
 151
 0152            totalCost = matchPredictionCost + bonusPredictionCost;
 153
 154            // Display results
 0155            var table = new Table();
 0156            table.Border(TableBorder.Rounded);
 157
 0158            if (settings.DetailedBreakdown)
 159            {
 160                // Add columns for detailed breakdown with reprediction support
 0161                table.AddColumn("Community Context");
 0162                table.AddColumn("Model");
 0163                table.AddColumn("Category");
 0164                table.AddColumn("Index 0", col => col.RightAligned());
 0165                table.AddColumn("Index 1", col => col.RightAligned());
 0166                table.AddColumn("Index 2+", col => col.RightAligned());
 0167                table.AddColumn("Total Count", col => col.RightAligned());
 0168                table.AddColumn("Total Cost (USD)", col => col.RightAligned());
 169
 170                // Group data by community context, model, and category to aggregate reprediction indices
 0171                var groupedData = detailedData
 0172                    .GroupBy(d => new { d.CommunityContext, d.Model, d.Category })
 0173                    .Select(g => new
 0174                    {
 0175                        g.Key.CommunityContext,
 0176                        g.Key.Model,
 0177                        g.Key.Category,
 0178                        Index0Count = g.Where(x => x.RepredictionIndex == 0).Sum(x => x.Count),
 0179                        Index0Cost = g.Where(x => x.RepredictionIndex == 0).Sum(x => x.Cost),
 0180                        Index1Count = g.Where(x => x.RepredictionIndex == 1).Sum(x => x.Count),
 0181                        Index1Cost = g.Where(x => x.RepredictionIndex == 1).Sum(x => x.Cost),
 0182                        Index2PlusCount = g.Where(x => x.RepredictionIndex >= 2).Sum(x => x.Count),
 0183                        Index2PlusCost = g.Where(x => x.RepredictionIndex >= 2).Sum(x => x.Cost),
 0184                        TotalCount = g.Sum(x => x.Count),
 0185                        TotalCost = g.Sum(x => x.Cost)
 0186                    })
 0187                    .OrderBy(g => g.CommunityContext)
 0188                    .ThenBy(g => g.Model)
 0189                    .ThenBy(g => g.Category)
 0190                    .ToList();
 191
 192                // Add rows for detailed breakdown with alternating styling
 0193                for (int i = 0; i < groupedData.Count; i++)
 194                {
 0195                    var data = groupedData[i];
 0196                    var isEvenRow = i % 2 == 0;
 197
 0198                    var index0Text = data.Index0Count > 0 ? $"{data.Index0Count} (${data.Index0Cost.ToString("F2", Cultu
 0199                    var index1Text = data.Index1Count > 0 ? $"{data.Index1Count} (${data.Index1Cost.ToString("F2", Cultu
 0200                    var index2PlusText = data.Index2PlusCount > 0 ? $"{data.Index2PlusCount} (${data.Index2PlusCost.ToSt
 201
 0202                    if (isEvenRow)
 203                    {
 204                        // Even rows - normal styling
 0205                        table.AddRow(
 0206                            data.CommunityContext,
 0207                            data.Model,
 0208                            data.Category,
 0209                            index0Text,
 0210                            index1Text,
 0211                            index2PlusText,
 0212                            data.TotalCount.ToString(CultureInfo.InvariantCulture),
 0213                            $"${data.TotalCost.ToString("F4", CultureInfo.InvariantCulture)}"
 0214                        );
 215                    }
 216                    else
 217                    {
 218                        // Odd rows - subtle blue tint for visual differentiation
 0219                        table.AddRow(
 0220                            $"[blue]{data.CommunityContext}[/]",
 0221                            $"[blue]{data.Model}[/]",
 0222                            $"[blue]{data.Category}[/]",
 0223                            $"[blue]{index0Text}[/]",
 0224                            $"[blue]{index1Text}[/]",
 0225                            $"[blue]{index2PlusText}[/]",
 0226                            $"[blue]{data.TotalCount.ToString(CultureInfo.InvariantCulture)}[/]",
 0227                            $"[blue]${data.TotalCost.ToString("F4", CultureInfo.InvariantCulture)}[/]"
 0228                        );
 229                    }
 230                }
 231
 232                // Add total row
 0233                if (detailedData.Any())
 234                {
 235                    // Calculate totals by reprediction index
 0236                    var totalIndex0Count = detailedData.Where(x => x.RepredictionIndex == 0).Sum(x => x.Count);
 0237                    var totalIndex0Cost = detailedData.Where(x => x.RepredictionIndex == 0).Sum(x => x.Cost);
 0238                    var totalIndex1Count = detailedData.Where(x => x.RepredictionIndex == 1).Sum(x => x.Count);
 0239                    var totalIndex1Cost = detailedData.Where(x => x.RepredictionIndex == 1).Sum(x => x.Cost);
 0240                    var totalIndex2PlusCount = detailedData.Where(x => x.RepredictionIndex >= 2).Sum(x => x.Count);
 0241                    var totalIndex2PlusCost = detailedData.Where(x => x.RepredictionIndex >= 2).Sum(x => x.Cost);
 242
 0243                    var totalIndex0Text = totalIndex0Count > 0 ? $"{totalIndex0Count} (${totalIndex0Cost.ToString("F2", 
 0244                    var totalIndex1Text = totalIndex1Count > 0 ? $"{totalIndex1Count} (${totalIndex1Cost.ToString("F2", 
 0245                    var totalIndex2PlusText = totalIndex2PlusCount > 0 ? $"{totalIndex2PlusCount} (${totalIndex2PlusCost
 246
 0247                    table.AddEmptyRow();
 0248                    table.AddRow(
 0249                        "[bold]Total[/]",
 0250                        "",
 0251                        "",
 0252                        $"[bold]{totalIndex0Text}[/]",
 0253                        $"[bold]{totalIndex1Text}[/]",
 0254                        $"[bold]{totalIndex2PlusText}[/]",
 0255                        $"[bold]{(matchPredictionCount + bonusPredictionCount).ToString(CultureInfo.InvariantCulture)}[/
 0256                        $"[bold]${totalCost.ToString("F4", CultureInfo.InvariantCulture)}[/]"
 0257                    );
 258                }
 259            }
 260            else
 261            {
 262                // Standard summary table
 0263                table.AddColumn("Category");
 0264                table.AddColumn("Count", col => col.RightAligned());
 0265                table.AddColumn("Cost (USD)", col => col.RightAligned());
 266
 0267                var rowIndex = 0;
 268
 269                // Add Match row
 0270                var isEvenRow = rowIndex % 2 == 0;
 0271                if (isEvenRow)
 272                {
 0273                    table.AddRow("Match", matchPredictionCount.ToString(CultureInfo.InvariantCulture), $"${matchPredicti
 274                }
 275                else
 276                {
 0277                    table.AddRow(
 0278                        "[blue]Match[/]",
 0279                        $"[blue]{matchPredictionCount.ToString(CultureInfo.InvariantCulture)}[/]",
 0280                        $"[blue]${matchPredictionCost.ToString("F4", CultureInfo.InvariantCulture)}[/]"
 0281                    );
 282                }
 0283                rowIndex++;
 284
 285                // Add Bonus row if applicable
 0286                if (settings.Bonus || settings.All)
 287                {
 0288                    isEvenRow = rowIndex % 2 == 0;
 0289                    if (isEvenRow)
 290                    {
 0291                        table.AddRow("Bonus", bonusPredictionCount.ToString(CultureInfo.InvariantCulture), $"${bonusPred
 292                    }
 293                    else
 294                    {
 0295                        table.AddRow(
 0296                            "[blue]Bonus[/]",
 0297                            $"[blue]{bonusPredictionCount.ToString(CultureInfo.InvariantCulture)}[/]",
 0298                            $"[blue]${bonusPredictionCost.ToString("F4", CultureInfo.InvariantCulture)}[/]"
 0299                        );
 300                    }
 301                }
 302
 0303                table.AddEmptyRow();
 0304                table.AddRow("[bold]Total[/]", $"[bold]{(matchPredictionCount + bonusPredictionCount).ToString(CultureIn
 305            }
 306
 0307            _console.Write(table);
 308
 0309            _console.MarkupLine($"[green]✓ Cost calculation completed[/]");
 310
 0311            return 0;
 312        }
 0313        catch (Exception ex)
 314        {
 0315            _logger.LogError(ex, "Failed to calculate costs");
 0316            _console.MarkupLine($"[red]✗ Failed to calculate costs: {ex.Message}[/]");
 0317            return 1;
 318        }
 0319    }
 320
 321    private List<int>? ParseMatchdays(CostSettings settings)
 322    {
 0323        if (settings.All || string.IsNullOrWhiteSpace(settings.Matchdays))
 0324            return null; // null means all matchdays
 325
 0326        if (settings.Matchdays.Trim().ToLowerInvariant() == "all")
 0327            return null;
 328
 329        try
 330        {
 0331            return settings.Matchdays
 0332                .Split(',', StringSplitOptions.RemoveEmptyEntries)
 0333                .Select(md => int.Parse(md.Trim()))
 0334                .ToList();
 335        }
 0336        catch (FormatException)
 337        {
 0338            throw new ArgumentException($"Invalid matchday format: {settings.Matchdays}. Use comma-separated numbers (e.
 339        }
 0340    }
 341
 342    private async Task<List<int>> GetAvailableMatchdays(FirestoreDb firestoreDb)
 343    {
 0344        var matchdays = new HashSet<int>();
 0345        var competition = "bundesliga-2025-26";
 346
 347        // Query match predictions for unique matchdays
 0348        var query = firestoreDb.Collection("match-predictions")
 0349            .WhereEqualTo("competition", competition);
 0350        var snapshot = await query.GetSnapshotAsync();
 351
 0352        foreach (var doc in snapshot.Documents)
 353        {
 0354            if (doc.TryGetValue<int>("matchday", out var matchday) && matchday > 0)
 355            {
 0356                matchdays.Add(matchday);
 357            }
 358        }
 359
 0360        return matchdays.OrderBy(m => m).ToList();
 0361    }
 362
 363    private List<string>? ParseModels(CostSettings settings)
 364    {
 0365        if (settings.All || string.IsNullOrWhiteSpace(settings.Models))
 0366            return null; // null means all models
 367
 0368        if (settings.Models.Trim().ToLowerInvariant() == "all")
 0369            return null;
 370
 0371        return settings.Models
 0372            .Split(',', StringSplitOptions.RemoveEmptyEntries)
 0373            .Select(m => m.Trim())
 0374            .Where(m => !string.IsNullOrWhiteSpace(m))
 0375            .ToList();
 376    }
 377
 378    private List<string>? ParseCommunityContexts(CostSettings settings)
 379    {
 0380        if (settings.All || string.IsNullOrWhiteSpace(settings.CommunityContexts))
 0381            return null; // null means all community contexts
 382
 0383        if (settings.CommunityContexts.Trim().ToLowerInvariant() == "all")
 0384            return null;
 385
 0386        return settings.CommunityContexts
 0387            .Split(',', StringSplitOptions.RemoveEmptyEntries)
 0388            .Select(cc => cc.Trim())
 0389            .Where(cc => !string.IsNullOrWhiteSpace(cc))
 0390            .ToList();
 391    }
 392
 393    private async Task<List<string>> GetAvailableModels(FirestoreDb firestoreDb)
 394    {
 0395        var models = new HashSet<string>();
 0396        var competition = "bundesliga-2025-26";
 397
 398        // Query match predictions for unique models
 0399        var matchQuery = firestoreDb.Collection("match-predictions")
 0400            .WhereEqualTo("competition", competition);
 0401        var matchSnapshot = await matchQuery.GetSnapshotAsync();
 402
 0403        foreach (var doc in matchSnapshot.Documents)
 404        {
 0405            if (doc.TryGetValue<string>("model", out var model) && !string.IsNullOrWhiteSpace(model))
 406            {
 0407                models.Add(model);
 408            }
 409        }
 410
 411        // Query bonus predictions for unique models
 0412        var bonusQuery = firestoreDb.Collection("bonus-predictions")
 0413            .WhereEqualTo("competition", competition);
 0414        var bonusSnapshot = await bonusQuery.GetSnapshotAsync();
 415
 0416        foreach (var doc in bonusSnapshot.Documents)
 417        {
 0418            if (doc.TryGetValue<string>("model", out var model) && !string.IsNullOrWhiteSpace(model))
 419            {
 0420                models.Add(model);
 421            }
 422        }
 423
 0424        return models.ToList();
 0425    }
 426
 427    private async Task<List<string>> GetAvailableCommunityContexts(FirestoreDb firestoreDb)
 428    {
 0429        var communityContexts = new HashSet<string>();
 0430        var competition = "bundesliga-2025-26";
 431
 432        // Query match predictions for unique community contexts
 0433        var matchQuery = firestoreDb.Collection("match-predictions")
 0434            .WhereEqualTo("competition", competition);
 0435        var matchSnapshot = await matchQuery.GetSnapshotAsync();
 436
 0437        foreach (var doc in matchSnapshot.Documents)
 438        {
 0439            if (doc.TryGetValue<string>("communityContext", out var context) && !string.IsNullOrWhiteSpace(context))
 440            {
 0441                communityContexts.Add(context);
 442            }
 443        }
 444
 445        // Query bonus predictions for unique community contexts
 0446        var bonusQuery = firestoreDb.Collection("bonus-predictions")
 0447            .WhereEqualTo("competition", competition);
 0448        var bonusSnapshot = await bonusQuery.GetSnapshotAsync();
 449
 0450        foreach (var doc in bonusSnapshot.Documents)
 451        {
 0452            if (doc.TryGetValue<string>("communityContext", out var context) && !string.IsNullOrWhiteSpace(context))
 453            {
 0454                communityContexts.Add(context);
 455            }
 456        }
 457
 0458        return communityContexts.ToList();
 0459    }
 460
 461    private async Task<CostConfiguration> LoadConfigurationFromFile(string configFilePath)
 462    {
 463        try
 464        {
 465            // Resolve relative paths
 0466            var resolvedPath = Path.IsPathRooted(configFilePath)
 0467                ? configFilePath
 0468                : Path.Combine(Directory.GetCurrentDirectory(), configFilePath);
 469
 0470            if (!File.Exists(resolvedPath))
 471            {
 0472                throw new FileNotFoundException($"Configuration file not found: {resolvedPath}");
 473            }
 474
 0475            _logger.LogInformation("Loading configuration from: {ConfigPath}", resolvedPath);
 476
 0477            var jsonContent = await File.ReadAllTextAsync(resolvedPath);
 0478            var options = new JsonSerializerOptions
 0479            {
 0480                PropertyNameCaseInsensitive = true,
 0481                AllowTrailingCommas = true,
 0482                ReadCommentHandling = JsonCommentHandling.Skip
 0483            };
 484
 0485            var config = JsonSerializer.Deserialize<CostConfiguration>(jsonContent, options);
 0486            if (config == null)
 487            {
 0488                throw new InvalidOperationException($"Failed to deserialize configuration from: {resolvedPath}");
 489            }
 490
 0491            return config;
 492        }
 0493        catch (JsonException ex)
 494        {
 0495            throw new InvalidOperationException($"Invalid JSON in configuration file: {configFilePath}. {ex.Message}", e
 496        }
 0497        catch (Exception ex)
 498        {
 0499            throw new InvalidOperationException($"Failed to load configuration from file: {configFilePath}. {ex.Message}
 500        }
 0501    }
 502
 503    private CostSettings MergeConfigurations(CostConfiguration fileConfig, CostSettings cliSettings)
 504    {
 0505        _logger.LogInformation("Merging file configuration with command line options (CLI options take precedence)");
 506
 507        // Create a new settings object with file config as base, CLI overrides
 0508        var mergedSettings = new CostSettings();
 509
 510        // Apply file config first (if values are not null/default)
 0511        if (!string.IsNullOrWhiteSpace(fileConfig.Matchdays))
 0512            mergedSettings.Matchdays = fileConfig.Matchdays;
 513
 0514        if (fileConfig.Bonus.HasValue)
 0515            mergedSettings.Bonus = fileConfig.Bonus.Value;
 516
 0517        if (!string.IsNullOrWhiteSpace(fileConfig.Models))
 0518            mergedSettings.Models = fileConfig.Models;
 519
 0520        if (!string.IsNullOrWhiteSpace(fileConfig.CommunityContexts))
 0521            mergedSettings.CommunityContexts = fileConfig.CommunityContexts;
 522
 0523        if (fileConfig.All.HasValue)
 0524            mergedSettings.All = fileConfig.All.Value;
 525
 0526        if (fileConfig.Verbose.HasValue)
 0527            mergedSettings.Verbose = fileConfig.Verbose.Value;
 528
 0529        if (fileConfig.DetailedBreakdown.HasValue)
 0530            mergedSettings.DetailedBreakdown = fileConfig.DetailedBreakdown.Value;
 531
 532        // Override with CLI settings (non-default values)
 0533        if (!string.IsNullOrWhiteSpace(cliSettings.Matchdays))
 534        {
 0535            mergedSettings.Matchdays = cliSettings.Matchdays;
 0536            if (mergedSettings.Verbose)
 0537                _logger.LogInformation("CLI override: Matchdays = {Value}", cliSettings.Matchdays);
 538        }
 539
 0540        if (cliSettings.Bonus) // Only override if explicitly set to true
 541        {
 0542            mergedSettings.Bonus = cliSettings.Bonus;
 0543            if (mergedSettings.Verbose)
 0544                _logger.LogInformation("CLI override: Bonus = {Value}", cliSettings.Bonus);
 545        }
 546
 0547        if (!string.IsNullOrWhiteSpace(cliSettings.Models))
 548        {
 0549            mergedSettings.Models = cliSettings.Models;
 0550            if (mergedSettings.Verbose)
 0551                _logger.LogInformation("CLI override: Models = {Value}", cliSettings.Models);
 552        }
 553
 0554        if (!string.IsNullOrWhiteSpace(cliSettings.CommunityContexts))
 555        {
 0556            mergedSettings.CommunityContexts = cliSettings.CommunityContexts;
 0557            if (mergedSettings.Verbose)
 0558                _logger.LogInformation("CLI override: CommunityContexts = {Value}", cliSettings.CommunityContexts);
 559        }
 560
 0561        if (cliSettings.All) // Only override if explicitly set to true
 562        {
 0563            mergedSettings.All = cliSettings.All;
 0564            if (mergedSettings.Verbose)
 0565                _logger.LogInformation("CLI override: All = {Value}", cliSettings.All);
 566        }
 567
 0568        if (cliSettings.Verbose) // Only override if explicitly set to true
 569        {
 0570            mergedSettings.Verbose = cliSettings.Verbose;
 571        }
 572
 0573        if (cliSettings.DetailedBreakdown) // Only override if explicitly set to true
 574        {
 0575            mergedSettings.DetailedBreakdown = cliSettings.DetailedBreakdown;
 0576            if (mergedSettings.Verbose)
 0577                _logger.LogInformation("CLI override: DetailedBreakdown = {Value}", cliSettings.DetailedBreakdown);
 578        }
 579
 580        // Always preserve the ConfigFile setting
 0581        mergedSettings.ConfigFile = cliSettings.ConfigFile;
 582
 0583        return mergedSettings;
 584    }
 585}