using Microsoft.Extensions.Configuration; using QuestionsApi.Models; using QuestionsApi.Services; const string accessedOn = "2026-07-30"; var apply = args.Contains("--apply", StringComparer.OrdinalIgnoreCase); var connectionString = Environment.GetEnvironmentVariable("STEP3_MONGODB_CONNECTION_STRING") ?? "mongodb://192.168.86.50:27017"; var databaseName = Environment.GetEnvironmentVariable("STEP3_MONGODB_DATABASE") ?? "promiscuity"; var questions = SeedCatalog.All.Select(Build).ToList(); var errors = questions .Select(question => (question.ExternalId, Error: ValidateAuthoredQuestion(question))) .Where(result => result.Error is not null) .ToList(); if (errors.Count > 0) { foreach (var error in errors) Console.Error.WriteLine($"{error.ExternalId}: {error.Error}"); return 1; } if (questions.Count != 50 || questions.Select(q => q.ExternalId).Distinct().Count() != 50) { Console.Error.WriteLine("Expected exactly 50 unique questions."); return 1; } var grouped = questions .GroupBy(question => question.System) .OrderBy(group => group.Key) .Select(group => $"{group.Key}: {group.Count()}"); Console.WriteLine($"Validated {questions.Count} difficulty-4 gap questions."); Console.WriteLine(string.Join(Environment.NewLine, grouped)); if (!apply) { Console.WriteLine("Dry run only. Pass --apply to upsert into the configured development database."); return 0; } var configuration = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary { ["MongoDB:ConnectionString"] = connectionString, ["MongoDB:DatabaseName"] = databaseName }).Build(); var store = new QuestionStore(new MongoDatabaseProvider(configuration)); var upserted = await store.UpsertManyByExternalIdAsync(questions); var saved = await store.GetByExternalIdsAsync(questions.Select(q => q.ExternalId)); var invalidSaved = saved.Values.Where(q => q.Difficulty != 4 || !q.IsPublished || q.Media.Count != 0 || q.References.Count == 0 || q.FlaggedForReviewUtc is not null).ToList(); if (upserted != 50 || saved.Count != 50 || invalidSaved.Count != 0) { Console.Error.WriteLine($"Verification failed: upserted={upserted}, found={saved.Count}, invalid={invalidSaved.Count}."); return 1; } Console.WriteLine($"Upserted and verified 50 questions in database '{databaseName}'."); return 0; string? ValidateAuthoredQuestion(UpsertQuestionRequest question) { var schemaError = QuestionValidator.Validate(question); if (schemaError is not null) return schemaError; if (!question.ExternalId.StartsWith("gap-d4-20260730-", StringComparison.Ordinal)) return "externalId must use the gap-d4-20260730 prefix"; if (question.Difficulty != 4 || !question.IsPublished) return "question must be published at difficulty 4"; if (question.Media.Count != 0) return "gap questions must not contain unreviewed media"; if (question.Tags.Count is < 3 or > 6) return "questions require 3-6 tags"; if (question.Choices.Count < 5) return "questions require one correct answer and at least four distractors"; if (question.ChoiceSelectionPercentages.Count != question.Choices.Count || question.ChoiceSelectionPercentages.Values.Any(value => value != 0)) return "every choice selection percentage must initialize to zero"; if (!question.ChoiceExplanations[question.CorrectChoiceId].StartsWith("Correct.", StringComparison.Ordinal)) return "correct choice explanation must begin with Correct."; if (question.ChoiceExplanations .Where(entry => entry.Key != question.CorrectChoiceId) .Any(entry => !entry.Value.StartsWith("Incorrect.", StringComparison.Ordinal))) return "incorrect choice explanations must begin with Incorrect."; foreach (var followup in question.WrongAnswerFollowups.Values) { if (followup.Choices.Count != 3 || followup.Choices.Select(choice => choice.Id).SequenceEqual(["A", "B", "C"]) is false) return "wrong-answer follow-ups must use ordered A-C choices"; } return null; } UpsertQuestionRequest Build(Seed seed) { var ids = Enumerable.Range(0, seed.Answers.Length) .Select(index => ((char)('A' + index)).ToString()) .ToArray(); var choices = ids.Zip(seed.Answers, (id, text) => new QuestionChoice { Id = id, Text = text }).ToList(); var choiceExplanations = ids.ToDictionary( id => id, id => id == "A" ? $"Correct. {seed.Explanation}" : $"Incorrect. {choices.Single(choice => choice.Id == id).Text} does not adequately address the decisive finding: {seed.KeyFact}. {seed.Takeaway}"); var distractorIds = ids.Skip(1).ToArray(); var wrongFollowups = distractorIds.ToDictionary(id => id, id => { var index = Array.IndexOf(distractorIds, id); var alternatives = new[] { distractorIds[(index + 1) % distractorIds.Length], distractorIds[(index + 2) % distractorIds.Length] }; return new WrongAnswerFollowup { Prompt = $"Which limitation of option {id} should have ruled it out in this case?", Choices = [ new() { Id = "A", Text = choiceExplanations[id] }, new() { Id = "B", Text = choiceExplanations[alternatives[0]] }, new() { Id = "C", Text = choiceExplanations[alternatives[1]] } ], CorrectChoiceId = "A", TeachingPoint = choiceExplanations[id] }; }); return new UpsertQuestionRequest { ExternalId = $"gap-d4-20260730-{seed.Slug}", Stem = seed.Stem, LeadIn = seed.LeadIn, Choices = choices, CorrectChoiceId = "A", Explanation = $"Correct. {seed.Explanation}", KeyTakeaway = seed.Takeaway, UsmlePearl = seed.Pearl, ReviewFollowups = [ new() { Prompt = "Which principle best generalizes from this case?", Choices = [ new() { Id = "A", Text = seed.Takeaway }, new() { Id = "B", Text = seed.ReviewDistractor1 }, new() { Id = "C", Text = seed.ReviewDistractor2 } ], CorrectChoiceId = "A", TeachingPoint = seed.Takeaway } ], ChoiceExplanations = choiceExplanations, WrongAnswerFollowups = wrongFollowups, ChoiceSelectionPercentages = ids.ToDictionary(id => id, _ => 0), Discipline = seed.Discipline, System = seed.System, PerformanceCategory = seed.PerformanceCategory, Difficulty = 4, Labs = [], Media = [], Tags = [seed.TopicTag, "difficulty 4", "gap coverage", seed.Discipline.ToLowerInvariant()], References = [ new() { Title = seed.ReferenceTitle, Publisher = seed.ReferencePublisher, Url = seed.ReferenceUrl, Supports = seed.ReferenceSupports, AccessedOn = accessedOn, Reviewer = "OpenAI Codex medical content review" } ], IsPublished = true }; } internal sealed record Seed( string Slug, string System, string Discipline, string PerformanceCategory, string TopicTag, string Stem, string LeadIn, string[] Answers, string Explanation, string Takeaway, string Pearl, string KeyFact, string ContextFact, string NeutralFact, string ReviewDistractor1, string ReviewDistractor2, string ReferenceTitle, string ReferencePublisher, string ReferenceUrl, string ReferenceSupports);