< Summary

Information
Class: Orchestrator.Commands.Observability.PrepareCommunityToDate.PrepareCommunityToDateCommand
Assembly: Orchestrator
File(s): /home/runner/work/KicktippAi/KicktippAi/src/Orchestrator/Commands/Observability/PrepareCommunityToDate/PrepareCommunityToDateCommand.cs
Line coverage
89%
Covered lines: 140
Uncovered lines: 16
Coverable lines: 156
Total lines: 253
Line coverage: 89.7%
Branch coverage
80%
Covered branches: 45
Total branches: 56
Branch coverage: 80.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
ExecuteAsync()86.36%454493.1%
GetStartsAt(...)50%8883.33%
ResolveOutputDirectory(...)50%4222.22%
WriteJsonFileAsync<T>(...)100%11100%
.ctor(...)100%11100%

File(s)

/home/runner/work/KicktippAi/KicktippAi/src/Orchestrator/Commands/Observability/PrepareCommunityToDate/PrepareCommunityToDateCommand.cs

#LineLine coverage
 1using System.Text.Json;
 2using EHonda.KicktippAi.Core;
 3using KicktippIntegration;
 4using Microsoft.Extensions.Logging;
 5using Orchestrator.Commands.Observability.Experiments;
 6using Orchestrator.Commands.Observability.ExportExperimentDataset;
 7using Orchestrator.Infrastructure.Factories;
 8using Spectre.Console;
 9using Spectre.Console.Cli;
 10
 11namespace Orchestrator.Commands.Observability.PrepareCommunityToDate;
 12
 13public sealed class PrepareCommunityToDateCommand : AsyncCommand<PrepareCommunityToDateSettings>
 14{
 15    private const string Competition = "bundesliga-2025-26";
 16
 17    private readonly IAnsiConsole _console;
 18    private readonly IKicktippClientFactory _kicktippClientFactory;
 19    private readonly ILogger<PrepareCommunityToDateCommand> _logger;
 20
 121    public PrepareCommunityToDateCommand(
 122        IAnsiConsole console,
 123        IKicktippClientFactory kicktippClientFactory,
 124        ILogger<PrepareCommunityToDateCommand> logger)
 25    {
 126        _console = console;
 127        _kicktippClientFactory = kicktippClientFactory;
 128        _logger = logger;
 129    }
 30
 31    protected override async Task<int> ExecuteAsync(CommandContext context, PrepareCommunityToDateSettings settings, Can
 32    {
 33        try
 34        {
 135            EnvironmentHelper.LoadCommunityKicktippCredentials(_logger, settings.CommunityContext);
 136            var kicktippClient = _kicktippClientFactory.CreateClient();
 137            PreparedExperimentSupport.ReportProgress(
 138                $"Preparing community-to-date artifact for '{settings.CommunityContext}'.");
 139            var cutoffMatchday = settings.CutoffMatchday ?? await kicktippClient.GetCurrentTippuebersichtMatchdayAsync(s
 140            PreparedExperimentSupport.ReportProgress(
 141                $"Using cutoff matchday {cutoffMatchday} for '{settings.CommunityContext}'.");
 142            var sourcePoolKey = string.IsNullOrWhiteSpace(settings.SourcePoolKey)
 143                ? $"through-md{cutoffMatchday:00}"
 144                : settings.SourcePoolKey.Trim();
 145            var sliceKey = string.IsNullOrWhiteSpace(settings.SliceKey)
 146                ? $"community-to-date-md{cutoffMatchday:00}"
 147                : settings.SliceKey.Trim();
 148            var sourceDatasetName = ExperimentArtifactSupport.BuildSourceDatasetName(settings.CommunityContext);
 149            var datasetName = settings.DatasetName
 150                ?? $"{sourceDatasetName}/community-to-date/{sourcePoolKey}/{sliceKey}";
 151            var outputDirectory = ResolveOutputDirectory(settings.OutputDirectory, settings.CommunityContext, sourcePool
 152            var sliceArtifactPath = Path.Combine(outputDirectory, "slice-dataset.json");
 153            var sliceManifestPath = Path.Combine(outputDirectory, "slice-manifest.json");
 54
 155            Directory.CreateDirectory(outputDirectory);
 56
 157            var sourceItems = new List<PreparedExperimentSourceItem>();
 158            var sourceItemsBySourceMatchId = new Dictionary<string, PreparedExperimentSourceItem>(StringComparer.Ordinal
 159            var participantBuilders = new Dictionary<string, ParticipantBuilder>(StringComparer.Ordinal);
 60
 161            for (var matchday = 1; matchday <= cutoffMatchday; matchday += 1)
 62            {
 163                PreparedExperimentSupport.ReportProgress(
 164                    $"Fetching Kicktipp community snapshot for matchday {matchday}/{cutoffMatchday}.");
 165                var snapshot = await kicktippClient.GetCommunityMatchdaySnapshotAsync(settings.CommunityContext, matchda
 166                if (snapshot is null)
 67                {
 068                    PreparedExperimentSupport.ReportProgress(
 069                        $"No community snapshot was available for matchday {matchday}; skipping.");
 070                    continue;
 71                }
 72
 173                foreach (var outcome in snapshot.Outcomes.Where(candidate => candidate.HasOutcome && candidate.HomeGoals
 74                {
 175                    var datasetItem = ExperimentArtifactSupport.BuildHostedDatasetItem(outcome, settings.CommunityContex
 176                    var sliceDatasetItemId = ExperimentArtifactSupport.BuildSliceDatasetItemId(datasetItem.Id, sliceKey)
 177                    var sourceItem = new PreparedExperimentSourceItem(
 178                        datasetItem.Id,
 179                        sliceDatasetItemId,
 180                        datasetItem.Id,
 181                        datasetItem.Metadata.Competition,
 182                        datasetItem.Metadata.Season,
 183                        datasetItem.Metadata.CommunityContext,
 184                        datasetItem.Metadata.Matchday,
 185                        datasetItem.Metadata.MatchdayLabel,
 186                        datasetItem.Metadata.HomeTeam,
 187                        datasetItem.Metadata.AwayTeam,
 188                        GetStartsAt(datasetItem),
 189                        datasetItem.Metadata.TippSpielId,
 190                        datasetItem.ExpectedOutput.HomeGoals,
 191                        datasetItem.ExpectedOutput.AwayGoals);
 92
 193                    if (sourceItemsBySourceMatchId.TryAdd(sourceItem.TippSpielId, sourceItem))
 94                    {
 195                        sourceItems.Add(sourceItem);
 96                    }
 97                }
 98
 199                foreach (var participant in snapshot.Participants)
 100                {
 1101                    if (!participantBuilders.TryGetValue(participant.ParticipantId, out var builder))
 102                    {
 1103                        builder = new ParticipantBuilder(participant.ParticipantId, participant.DisplayName);
 1104                        participantBuilders.Add(participant.ParticipantId, builder);
 105                    }
 106
 1107                    foreach (var prediction in participant.Predictions)
 108                    {
 1109                        var sourceMatchId = prediction.TippSpielId ?? prediction.SourceMatchId;
 1110                        if (!sourceItemsBySourceMatchId.TryGetValue(sourceMatchId, out var sourceItem))
 111                        {
 112                            continue;
 113                        }
 114
 1115                        builder.Predictions[sourceItem.SourceDatasetItemId] = new PreparedExperimentParticipantPredictio
 1116                        {
 1117                            SourceDatasetItemId = sourceItem.SourceDatasetItemId,
 1118                            Status = prediction.Status == KicktippCommunityPredictionStatus.Placed ? "placed" : "missed"
 1119                            HomeGoals = prediction.Prediction?.HomeGoals,
 1120                            AwayGoals = prediction.Prediction?.AwayGoals,
 1121                            KicktippPoints = prediction.AwardedPoints
 1122                        };
 123                    }
 124                }
 125
 1126                PreparedExperimentSupport.ReportProgress(
 1127                    $"Processed matchday {matchday}/{cutoffMatchday}: {snapshot.Outcomes.Count} outcome candidate(s), {s
 128            }
 129
 1130            if (sourceItems.Count == 0)
 131            {
 0132                throw new InvalidOperationException("No completed Kicktipp matches were found for the requested communit
 133            }
 134
 1135            var orderedSourceItems = sourceItems
 1136                .OrderBy(item => item.Matchday)
 1137                .ThenBy(item => item.SourceDatasetItemId, StringComparer.Ordinal)
 1138                .ToList();
 1139            var bundle = PreparedExperimentBundleBuilder.Build(
 1140                orderedSourceItems,
 1141                settings.CommunityContext,
 1142                sourceDatasetName,
 1143                datasetName,
 1144                sliceKey,
 1145                "community-to-date",
 1146                "community-to-date",
 1147                sourcePoolKey,
 1148                null);
 149
 1150            var manifest = bundle.Manifest with
 1151            {
 1152                TaskType = "community-to-date",
 1153                CutoffMatchday = cutoffMatchday,
 1154                Participants = participantBuilders.Values
 1155                    .OrderBy(builder => builder.DisplayName, StringComparer.OrdinalIgnoreCase)
 1156                    .Select(builder => new PreparedExperimentParticipantManifest
 1157                    {
 1158                        ParticipantId = builder.ParticipantId,
 1159                        DisplayName = builder.DisplayName,
 1160                        Predictions = builder.Predictions.Values
 1161                            .OrderBy(prediction => prediction.SourceDatasetItemId, StringComparer.Ordinal)
 1162                            .ToList()
 1163                    })
 1164                    .ToList()
 1165            };
 166
 1167            await WriteJsonFileAsync(sliceArtifactPath, bundle.Artifact, cancellationToken);
 1168            await WriteJsonFileAsync(sliceManifestPath, manifest, cancellationToken);
 169
 1170            PreparedExperimentSupport.ReportProgress(
 1171                $"Prepared community-to-date artifact with {manifest.SampleSize} item(s) and {manifest.Participants.Coun
 172
 1173            var summary = new
 1174            {
 1175                mode = "community-to-date",
 1176                communityContext = settings.CommunityContext,
 1177                cutoffMatchday,
 1178                sourceDatasetName,
 1179                datasetName = manifest.SliceDatasetName,
 1180                manifest.SourcePoolKey,
 1181                manifest.SliceKey,
 1182                manifest.SampleSize,
 1183                participantCount = manifest.Participants.Count,
 1184                manifest.SelectedItemIdsHash,
 1185                outputDirectory,
 1186                sliceArtifactPath,
 1187                sliceManifestPath
 1188            };
 189
 1190            _console.WriteLine(JsonSerializer.Serialize(summary, PreparedExperimentCommandSupport.JsonOptions));
 1191            return 0;
 192        }
 0193        catch (Exception ex)
 194        {
 0195            _logger.LogError(ex, "Error preparing community-to-date experiment artifact");
 0196            _console.MarkupLine($"[red]Error:[/] {Markup.Escape(ex.Message)}");
 0197            return 1;
 198        }
 1199    }
 200
 201    private static string GetStartsAt(HostedMatchExperimentDatasetItem item)
 202    {
 1203        if (item.Input.ValueKind != JsonValueKind.Object
 1204            || !item.Input.TryGetProperty("startsAt", out var startsAt)
 1205            || startsAt.ValueKind != JsonValueKind.String
 1206            || string.IsNullOrWhiteSpace(startsAt.GetString()))
 207        {
 0208            throw new InvalidOperationException($"Dataset item '{item.Id}' is missing input.startsAt.");
 209        }
 210
 1211        return startsAt.GetString()!;
 212    }
 213
 214    private static string ResolveOutputDirectory(
 215        string? outputDirectoryOverride,
 216        string communityContext,
 217        string sourcePoolKey,
 218        string sliceKey)
 219    {
 1220        if (!string.IsNullOrWhiteSpace(outputDirectoryOverride))
 221        {
 1222            return Path.GetFullPath(outputDirectoryOverride);
 223        }
 224
 0225        return Path.GetFullPath(Path.Combine(
 0226            "artifacts",
 0227            "langfuse-experiments",
 0228            "community-to-date",
 0229            ExperimentArtifactSupport.Slugify(communityContext),
 0230            sourcePoolKey,
 0231            sliceKey));
 232    }
 233
 234    private static Task WriteJsonFileAsync<T>(string path, T value, CancellationToken cancellationToken)
 235    {
 1236        return File.WriteAllTextAsync(path, JsonSerializer.Serialize(value, PreparedExperimentCommandSupport.JsonOptions
 237    }
 238
 239    private sealed class ParticipantBuilder
 240    {
 1241        public ParticipantBuilder(string participantId, string displayName)
 242        {
 1243            ParticipantId = participantId;
 1244            DisplayName = displayName;
 1245        }
 246
 247        public string ParticipantId { get; }
 248
 249        public string DisplayName { get; }
 250
 1251        public Dictionary<string, PreparedExperimentParticipantPrediction> Predictions { get; } = new(StringComparer.Ord
 252    }
 253}