106 lines
9.2 KiB
C#
106 lines
9.2 KiB
C#
using Microsoft.Extensions.Configuration;
|
|
using MongoDB.Driver;
|
|
using QuestionsApi.Models;
|
|
using QuestionsApi.Services;
|
|
|
|
const string accessedOn = "2026-07-31";
|
|
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")
|
|
?? new MongoUrl(connectionString).DatabaseName
|
|
?? "promiscuity";
|
|
var questions = SeedCatalog.All.Select(Build).ToList();
|
|
var errors = questions.Select(q => (q.ExternalId, Error: Validate(q))).Where(x => x.Error is not null).ToList();
|
|
foreach (var error in errors) Console.Error.WriteLine($"{error.ExternalId}: {error.Error}");
|
|
if (errors.Count > 0) return 1;
|
|
if (questions.Count != 50 || questions.Select(q => q.ExternalId).Distinct().Count() != 50)
|
|
{
|
|
Console.Error.WriteLine("Expected exactly 50 unique questions.");
|
|
return 1;
|
|
}
|
|
Console.WriteLine($"Validated {questions.Count} difficulty-4 gap questions.");
|
|
foreach (var group in questions.GroupBy(q => q.System).OrderBy(g => g.Key)) Console.WriteLine($"{group.Key}: {group.Count()}");
|
|
if (!apply)
|
|
{
|
|
Console.WriteLine("Dry run only. Pass --apply to upsert into the configured database.");
|
|
return 0;
|
|
}
|
|
var configuration = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary<string, string?>
|
|
{
|
|
["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 invalid = saved.Values.Count(q => q.Difficulty != 4 || !q.IsPublished || q.Media.Count != 0 || q.References.Count == 0 || q.FlaggedForReviewUtc is not null);
|
|
if (upserted != 50 || saved.Count != 50 || invalid != 0)
|
|
{
|
|
Console.Error.WriteLine($"Verification failed: upserted={upserted}, found={saved.Count}, invalid={invalid}.");
|
|
return 1;
|
|
}
|
|
Console.WriteLine($"Upserted and verified 50 questions in database '{databaseName}'.");
|
|
return 0;
|
|
|
|
string? Validate(UpsertQuestionRequest q)
|
|
{
|
|
var error = QuestionValidator.Validate(q);
|
|
if (error is not null) return error;
|
|
if (!q.ExternalId.StartsWith("gap-d4-20260731-", StringComparison.Ordinal)) return "incorrect externalId prefix";
|
|
if (q.Difficulty != 4 || !q.IsPublished) return "question must be published at difficulty 4";
|
|
if (q.Choices.Count < 5) return "one correct answer and at least four distractors are required";
|
|
if (q.Media.Count != 0) return "unreviewed media is prohibited";
|
|
if (q.Tags.Count is < 3 or > 6) return "3-6 tags required";
|
|
if (q.ChoiceSelectionPercentages.Count != q.Choices.Count || q.ChoiceSelectionPercentages.Values.Any(v => v != 0)) return "choice percentages must initialize to zero";
|
|
if (!q.ChoiceExplanations[q.CorrectChoiceId].StartsWith("Correct.")) return "correct explanation prefix required";
|
|
if (q.ChoiceExplanations.Where(x => x.Key != q.CorrectChoiceId).Any(x => !x.Value.StartsWith("Incorrect."))) return "incorrect explanation prefix required";
|
|
return null;
|
|
}
|
|
|
|
UpsertQuestionRequest Build(Seed s)
|
|
{
|
|
var ids = Enumerable.Range(0, s.Answers.Length).Select(i => ((char)('A' + i)).ToString()).ToArray();
|
|
var choices = ids.Zip(s.Answers, (id, text) => new QuestionChoice { Id = id, Text = text }).ToList();
|
|
var explanations = ids.ToDictionary(id => id, id => id == "A" ? $"Correct. {s.Rationale}" : $"Incorrect. {choices.Single(c => c.Id == id).Text} does not fit the decisive finding: {s.KeyFact}. {s.Takeaway}");
|
|
var wrong = ids.Skip(1).ToDictionary(id => id, _ => new WrongAnswerFollowup
|
|
{
|
|
Prompt = $"Which finding most strongly supports {s.Answers[0]}?",
|
|
Choices = [new() { Id = "A", Text = s.KeyFact }, new() { Id = "B", Text = s.ContextFact }, new() { Id = "C", Text = s.NeutralFact }],
|
|
CorrectChoiceId = "A", TeachingPoint = s.Takeaway
|
|
});
|
|
var reference = References.For(s.Reference);
|
|
return new UpsertQuestionRequest
|
|
{
|
|
ExternalId = $"gap-d4-20260731-{s.Slug}", Stem = s.Stem, LeadIn = s.LeadIn, Choices = choices, CorrectChoiceId = "A",
|
|
Explanation = $"Correct. {s.Rationale}", KeyTakeaway = s.Takeaway, UsmlePearl = s.Takeaway,
|
|
ReviewFollowups = [new() { Prompt = "Which principle best generalizes from this case?", Choices = [new() { Id = "A", Text = s.Takeaway }, new() { Id = "B", Text = "Delay action until every possible diagnosis is excluded." }, new() { Id = "C", Text = "Base management on a nondispositive finding alone." }], CorrectChoiceId = "A", TeachingPoint = s.Takeaway }],
|
|
ChoiceExplanations = explanations, WrongAnswerFollowups = wrong, ChoiceSelectionPercentages = ids.ToDictionary(id => id, _ => 0),
|
|
Discipline = s.Discipline, System = s.System, PerformanceCategory = s.Task, Difficulty = 4, Labs = [], Media = [],
|
|
Tags = [s.Topic, "difficulty 4", "gap coverage", s.Discipline.ToLowerInvariant()],
|
|
References = [new() { Title = reference.Title, Publisher = reference.Publisher, Url = reference.Url, Supports = reference.Supports, AccessedOn = accessedOn, Reviewer = "OpenAI Codex medical content review" }], IsPublished = true
|
|
};
|
|
}
|
|
|
|
internal sealed record Seed(string Slug, string System, string Discipline, string Task, string Topic, string Stem, string LeadIn, string[] Answers, string Rationale, string Takeaway, string KeyFact, string ContextFact, string NeutralFact, string Reference);
|
|
internal sealed record ReferenceSeed(string Title, string Publisher, string Url, string Supports);
|
|
internal static class References
|
|
{
|
|
internal static ReferenceSeed For(string key) => key switch
|
|
{
|
|
"APA" => new("Clinical Practice Guidelines", "American Psychiatric Association", "https://www.psychiatry.org/psychiatrists/practice/clinical-practice-guidelines", "Evidence-based assessment and treatment of psychiatric disorders."),
|
|
"SAMHSA" => new("National Guidelines for Behavioral Health Crisis Care", "Substance Abuse and Mental Health Services Administration", "https://www.samhsa.gov/find-help/implementing-behavioral-health-crisis-care", "Crisis assessment, stabilization, and linkage to care."),
|
|
"CDC-MEC" => new("U.S. Medical Eligibility Criteria for Contraceptive Use, 2024", "Centers for Disease Control and Prevention", "https://www.cdc.gov/mmwr/volumes/73/rr/rr7304a1.htm", "Safe contraceptive selection in patients with medical comorbidities."),
|
|
"CDC-STI" => new("Sexually Transmitted Infections Treatment Guidelines", "Centers for Disease Control and Prevention", "https://www.cdc.gov/std/treatment-guidelines/default.htm", "Diagnosis, treatment, partner management, and prevention of sexually transmitted infections."),
|
|
"USPSTF" => new("Recommendation Topics", "U.S. Preventive Services Task Force", "https://www.uspreventiveservicestaskforce.org/uspstf/recommendation-topics", "Evidence-based screening and preventive service recommendations."),
|
|
"ACOG" => new("Clinical Guidance", "American College of Obstetricians and Gynecologists", "https://www.acog.org/clinical", "Evidence-based obstetric and gynecologic clinical management."),
|
|
"AAO-HNS" => new("Clinical Practice Guidelines", "American Academy of Otolaryngology-Head and Neck Surgery", "https://www.entnet.org/quality-practice/quality-products/clinical-practice-guidelines/", "Evidence-based evaluation and treatment of otolaryngologic disease."),
|
|
"AAAAI" => new("Statements and Practice Parameters", "American Academy of Allergy, Asthma & Immunology", "https://www.aaaai.org/practice-management/statements-practice-parameters", "Diagnosis and management of allergic and immunologic disease."),
|
|
"ENDO" => new("Clinical Practice Guidelines", "Endocrine Society", "https://www.endocrine.org/clinical-practice-guidelines", "Evidence-based diagnosis and management of endocrine emergencies and disorders."),
|
|
"AUA" => new("Clinical Guidelines", "American Urological Association", "https://www.auanet.org/guidelines-and-quality/guidelines", "Evidence-based emergency management of urologic conditions."),
|
|
"IDSA-SSTI" => new("Practice Guidelines for Skin and Soft Tissue Infections", "Infectious Diseases Society of America", "https://www.idsociety.org/practice-guideline/skin-and-soft-tissue-infections/", "Recognition and urgent surgical treatment of necrotizing soft-tissue infection."),
|
|
"FDA-LAM" => new("Lamotrigine Drug Safety Communication", "U.S. Food and Drug Administration", "https://www.fda.gov/drugs/drug-safety-and-availability/fda-warns-serious-immune-system-reaction-seizure-and-mental-health-medicine-lamotrigine-lamictal", "Serious cutaneous and systemic reactions associated with lamotrigine."),
|
|
"AHRQ" => new("Patient Safety Network", "Agency for Healthcare Research and Quality", "https://psnet.ahrq.gov/", "Systems approaches to patient safety and quality improvement."),
|
|
"AMA" => new("Code of Medical Ethics", "American Medical Association", "https://code-medical-ethics.ama-assn.org/", "Ethical duties involving consent, confidentiality, capacity, and professionalism."),
|
|
_ => throw new ArgumentOutOfRangeException(nameof(key), key, null)
|
|
};
|
|
}
|