using Microsoft.Extensions.Configuration; using QuestionsApi.Models; using System.Text.Json; namespace QuestionsApi.Services; public static class DynamicReviewExportCommand { public static async Task RunAsync(string[] args, TextWriter output) { static string Argument(string[] values, string name, string fallback = "") { var index = Array.FindIndex(values, value => value.Equals(name, StringComparison.OrdinalIgnoreCase)); return index >= 0 && index + 1 < values.Length ? values[index + 1] : fallback; } var source = Argument(args, "--source", Path.Combine(AppContext.BaseDirectory, "content", "dynamic_questions")); var destination = Argument(args, "--output"); if (!Directory.Exists(source) || string.IsNullOrWhiteSpace(destination)) { await output.WriteLineAsync("Usage: export-dynamic-review --source --output "); return 2; } var jsonOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web) { PropertyNameCaseInsensitive = true, WriteIndented = true }; var blueprints = new List(); foreach (var file in Directory.EnumerateFiles(source, "*.json", SearchOption.AllDirectories)) blueprints.AddRange(JsonSerializer.Deserialize>( await File.ReadAllTextAsync(file), jsonOptions) ?? []); blueprints = blueprints.Where(value => value.IsPublished && value.QualityStandardVersion >= 2).ToList(); var configuration = new ConfigurationBuilder() .AddJsonFile("appsettings.json", optional: true) .AddEnvironmentVariables() .Build(); var authored = (await new QuestionStore(new MongoDatabaseProvider(configuration)).GetPublishedAsync()) .OrderBy(value => value.ExternalId, StringComparer.Ordinal) .Take(blueprints.Count * 3) .ToList(); if (authored.Count < blueprints.Count * 3) throw new InvalidOperationException("Not enough published authored questions for a balanced review packet."); var generator = new DynamicQuestionGenerator(); var entries = new List<(Question Question, string Source, string Detail)>(); foreach (var blueprint in blueprints) for (var seed = 0; seed < 3; seed++) { var generated = generator.Generate(blueprint, $"review-{seed}", $"review-{blueprint.ExternalId}-{seed}"); entries.Add((generated.Question, "dynamic", $"{blueprint.ExternalId}:{generated.CaseFamilyId}")); } entries.AddRange(authored.Select(value => (value, "authored", value.ExternalId))); var ordered = entries.OrderBy(value => Convert.ToHexString( System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(value.Detail)))).ToList(); Directory.CreateDirectory(destination); var packet = ordered.Select((entry, index) => new { reviewId = $"Q{index + 1:D2}", entry.Question.Stem, entry.Question.LeadIn, entry.Question.Choices, entry.Question.Labs, entry.Question.Explanation, entry.Question.ChoiceExplanations, entry.Question.ReviewFollowups, entry.Question.WrongAnswerFollowups, scoring = new { realism = 0, distractors = 0, explanation = 0, step3Relevance = 0, criticalIssue = "", predictedSource = "" } }); var key = ordered.Select((entry, index) => new { reviewId = $"Q{index + 1:D2}", entry.Source, entry.Detail }); await File.WriteAllTextAsync(Path.Combine(destination, "review-packet.json"), JsonSerializer.Serialize(packet, jsonOptions)); await File.WriteAllTextAsync(Path.Combine(destination, "answer-key.json"), JsonSerializer.Serialize(key, jsonOptions)); await output.WriteLineAsync($"Exported {ordered.Count} blinded questions to {destination}."); return 0; } }