< Summary

Information
Class: Orchestrator.Commands.Observability.PrepareCommunityToDate.PrepareCommunityToDateCommand.ParticipantBuilder
Assembly: Orchestrator
File(s): /home/runner/work/KicktippAi/KicktippAi/src/Orchestrator/Commands/Observability/PrepareCommunityToDate/PrepareCommunityToDateCommand.cs
Line coverage
100%
Covered lines: 5
Uncovered lines: 0
Coverable lines: 5
Total lines: 253
Line coverage: 100%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.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
 21    public PrepareCommunityToDateCommand(
 22        IAnsiConsole console,
 23        IKicktippClientFactory kicktippClientFactory,
 24        ILogger<PrepareCommunityToDateCommand> logger)
 25    {
 26        _console = console;
 27        _kicktippClientFactory = kicktippClientFactory;
 28        _logger = logger;
 29    }
 30
 31    protected override async Task<int> ExecuteAsync(CommandContext context, PrepareCommunityToDateSettings settings, Can
 32    {
 33        try
 34        {
 35            EnvironmentHelper.LoadCommunityKicktippCredentials(_logger, settings.CommunityContext);
 36            var kicktippClient = _kicktippClientFactory.CreateClient();
 37            PreparedExperimentSupport.ReportProgress(
 38                $"Preparing community-to-date artifact for '{settings.CommunityContext}'.");
 39            var cutoffMatchday = settings.CutoffMatchday ?? await kicktippClient.GetCurrentTippuebersichtMatchdayAsync(s
 40            PreparedExperimentSupport.ReportProgress(
 41                $"Using cutoff matchday {cutoffMatchday} for '{settings.CommunityContext}'.");
 42            var sourcePoolKey = string.IsNullOrWhiteSpace(settings.SourcePoolKey)
 43                ? $"through-md{cutoffMatchday:00}"
 44                : settings.SourcePoolKey.Trim();
 45            var sliceKey = string.IsNullOrWhiteSpace(settings.SliceKey)
 46                ? $"community-to-date-md{cutoffMatchday:00}"
 47                : settings.SliceKey.Trim();
 48            var sourceDatasetName = ExperimentArtifactSupport.BuildSourceDatasetName(settings.CommunityContext);
 49            var datasetName = settings.DatasetName
 50                ?? $"{sourceDatasetName}/community-to-date/{sourcePoolKey}/{sliceKey}";
 51            var outputDirectory = ResolveOutputDirectory(settings.OutputDirectory, settings.CommunityContext, sourcePool
 52            var sliceArtifactPath = Path.Combine(outputDirectory, "slice-dataset.json");
 53            var sliceManifestPath = Path.Combine(outputDirectory, "slice-manifest.json");
 54
 55            Directory.CreateDirectory(outputDirectory);
 56
 57            var sourceItems = new List<PreparedExperimentSourceItem>();
 58            var sourceItemsBySourceMatchId = new Dictionary<string, PreparedExperimentSourceItem>(StringComparer.Ordinal
 59            var participantBuilders = new Dictionary<string, ParticipantBuilder>(StringComparer.Ordinal);
 60
 61            for (var matchday = 1; matchday <= cutoffMatchday; matchday += 1)
 62            {
 63                PreparedExperimentSupport.ReportProgress(
 64                    $"Fetching Kicktipp community snapshot for matchday {matchday}/{cutoffMatchday}.");
 65                var snapshot = await kicktippClient.GetCommunityMatchdaySnapshotAsync(settings.CommunityContext, matchda
 66                if (snapshot is null)
 67                {
 68                    PreparedExperimentSupport.ReportProgress(
 69                        $"No community snapshot was available for matchday {matchday}; skipping.");
 70                    continue;
 71                }
 72
 73                foreach (var outcome in snapshot.Outcomes.Where(candidate => candidate.HasOutcome && candidate.HomeGoals
 74                {
 75                    var datasetItem = ExperimentArtifactSupport.BuildHostedDatasetItem(outcome, settings.CommunityContex
 76                    var sliceDatasetItemId = ExperimentArtifactSupport.BuildSliceDatasetItemId(datasetItem.Id, sliceKey)
 77                    var sourceItem = new PreparedExperimentSourceItem(
 78                        datasetItem.Id,
 79                        sliceDatasetItemId,
 80                        datasetItem.Id,
 81                        datasetItem.Metadata.Competition,
 82                        datasetItem.Metadata.Season,
 83                        datasetItem.Metadata.CommunityContext,
 84                        datasetItem.Metadata.Matchday,
 85                        datasetItem.Metadata.MatchdayLabel,
 86                        datasetItem.Metadata.HomeTeam,
 87                        datasetItem.Metadata.AwayTeam,
 88                        GetStartsAt(datasetItem),
 89                        datasetItem.Metadata.TippSpielId,
 90                        datasetItem.ExpectedOutput.HomeGoals,
 91                        datasetItem.ExpectedOutput.AwayGoals);
 92
 93                    if (sourceItemsBySourceMatchId.TryAdd(sourceItem.TippSpielId, sourceItem))
 94                    {
 95                        sourceItems.Add(sourceItem);
 96                    }
 97                }
 98
 99                foreach (var participant in snapshot.Participants)
 100                {
 101                    if (!participantBuilders.TryGetValue(participant.ParticipantId, out var builder))
 102                    {
 103                        builder = new ParticipantBuilder(participant.ParticipantId, participant.DisplayName);
 104                        participantBuilders.Add(participant.ParticipantId, builder);
 105                    }
 106
 107                    foreach (var prediction in participant.Predictions)
 108                    {
 109                        var sourceMatchId = prediction.TippSpielId ?? prediction.SourceMatchId;
 110                        if (!sourceItemsBySourceMatchId.TryGetValue(sourceMatchId, out var sourceItem))
 111                        {
 112                            continue;
 113                        }
 114
 115                        builder.Predictions[sourceItem.SourceDatasetItemId] = new PreparedExperimentParticipantPredictio
 116                        {
 117                            SourceDatasetItemId = sourceItem.SourceDatasetItemId,
 118                            Status = prediction.Status == KicktippCommunityPredictionStatus.Placed ? "placed" : "missed"
 119                            HomeGoals = prediction.Prediction?.HomeGoals,
 120                            AwayGoals = prediction.Prediction?.AwayGoals,
 121                            KicktippPoints = prediction.AwardedPoints
 122                        };
 123                    }
 124                }
 125
 126                PreparedExperimentSupport.ReportProgress(
 127                    $"Processed matchday {matchday}/{cutoffMatchday}: {snapshot.Outcomes.Count} outcome candidate(s), {s
 128            }
 129
 130            if (sourceItems.Count == 0)
 131            {
 132                throw new InvalidOperationException("No completed Kicktipp matches were found for the requested communit
 133            }
 134
 135            var orderedSourceItems = sourceItems
 136                .OrderBy(item => item.Matchday)
 137                .ThenBy(item => item.SourceDatasetItemId, StringComparer.Ordinal)
 138                .ToList();
 139            var bundle = PreparedExperimentBundleBuilder.Build(
 140                orderedSourceItems,
 141                settings.CommunityContext,
 142                sourceDatasetName,
 143                datasetName,
 144                sliceKey,
 145                "community-to-date",
 146                "community-to-date",
 147                sourcePoolKey,
 148                null);
 149
 150            var manifest = bundle.Manifest with
 151            {
 152                TaskType = "community-to-date",
 153                CutoffMatchday = cutoffMatchday,
 154                Participants = participantBuilders.Values
 155                    .OrderBy(builder => builder.DisplayName, StringComparer.OrdinalIgnoreCase)
 156                    .Select(builder => new PreparedExperimentParticipantManifest
 157                    {
 158                        ParticipantId = builder.ParticipantId,
 159                        DisplayName = builder.DisplayName,
 160                        Predictions = builder.Predictions.Values
 161                            .OrderBy(prediction => prediction.SourceDatasetItemId, StringComparer.Ordinal)
 162                            .ToList()
 163                    })
 164                    .ToList()
 165            };
 166
 167            await WriteJsonFileAsync(sliceArtifactPath, bundle.Artifact, cancellationToken);
 168            await WriteJsonFileAsync(sliceManifestPath, manifest, cancellationToken);
 169
 170            PreparedExperimentSupport.ReportProgress(
 171                $"Prepared community-to-date artifact with {manifest.SampleSize} item(s) and {manifest.Participants.Coun
 172
 173            var summary = new
 174            {
 175                mode = "community-to-date",
 176                communityContext = settings.CommunityContext,
 177                cutoffMatchday,
 178                sourceDatasetName,
 179                datasetName = manifest.SliceDatasetName,
 180                manifest.SourcePoolKey,
 181                manifest.SliceKey,
 182                manifest.SampleSize,
 183                participantCount = manifest.Participants.Count,
 184                manifest.SelectedItemIdsHash,
 185                outputDirectory,
 186                sliceArtifactPath,
 187                sliceManifestPath
 188            };
 189
 190            _console.WriteLine(JsonSerializer.Serialize(summary, PreparedExperimentCommandSupport.JsonOptions));
 191            return 0;
 192        }
 193        catch (Exception ex)
 194        {
 195            _logger.LogError(ex, "Error preparing community-to-date experiment artifact");
 196            _console.MarkupLine($"[red]Error:[/] {Markup.Escape(ex.Message)}");
 197            return 1;
 198        }
 199    }
 200
 201    private static string GetStartsAt(HostedMatchExperimentDatasetItem item)
 202    {
 203        if (item.Input.ValueKind != JsonValueKind.Object
 204            || !item.Input.TryGetProperty("startsAt", out var startsAt)
 205            || startsAt.ValueKind != JsonValueKind.String
 206            || string.IsNullOrWhiteSpace(startsAt.GetString()))
 207        {
 208            throw new InvalidOperationException($"Dataset item '{item.Id}' is missing input.startsAt.");
 209        }
 210
 211        return startsAt.GetString()!;
 212    }
 213
 214    private static string ResolveOutputDirectory(
 215        string? outputDirectoryOverride,
 216        string communityContext,
 217        string sourcePoolKey,
 218        string sliceKey)
 219    {
 220        if (!string.IsNullOrWhiteSpace(outputDirectoryOverride))
 221        {
 222            return Path.GetFullPath(outputDirectoryOverride);
 223        }
 224
 225        return Path.GetFullPath(Path.Combine(
 226            "artifacts",
 227            "langfuse-experiments",
 228            "community-to-date",
 229            ExperimentArtifactSupport.Slugify(communityContext),
 230            sourcePoolKey,
 231            sliceKey));
 232    }
 233
 234    private static Task WriteJsonFileAsync<T>(string path, T value, CancellationToken cancellationToken)
 235    {
 236        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}

Methods/Properties

.ctor(string, string)