Add question bank mastery paths and gap questions
This commit is contained in:
@@ -77,6 +77,24 @@ Promotion is idempotent and does not delete or implicitly unpublish PRD content.
|
||||
|
||||
## Verification and rollback
|
||||
|
||||
## Step 3 mastery system backfill
|
||||
|
||||
The mastery dashboard uses a canonical 14-system classification stored on each
|
||||
question. After deploying the mastery-capable API, inspect and apply the
|
||||
idempotent backfill in UAT:
|
||||
|
||||
```bash
|
||||
kubectl exec deployment/step3-api -n step3-uat -- \
|
||||
dotnet QuestionsApi.dll backfill-step3-systems
|
||||
kubectl exec deployment/step3-api -n step3-uat -- \
|
||||
dotnet QuestionsApi.dll backfill-step3-systems --apply --confirm BACKFILL_STEP3_SYSTEMS
|
||||
```
|
||||
|
||||
Review the reported unmapped count before acceptance testing. Repeat the dry run
|
||||
and confirmed apply in `step3-prd` after the tested revision is promoted to the
|
||||
production branch. The command is safe to rerun and does not modify attempts or
|
||||
user data.
|
||||
|
||||
## Rz Coin Store attempt-cycle migration
|
||||
|
||||
The QBank reset preserves every attempt and increments a user's eligibility cycle. After deploying
|
||||
|
||||
@@ -72,6 +72,9 @@ public sealed class DashboardTests
|
||||
[InlineData("Pregnancy", "Pregnancy/Childbirth & Female Reproductive System & Breast")]
|
||||
[InlineData("Biostatistics", "Biostatistics & Epidemiology/Population Health & Interpretation of the Medical Literature")]
|
||||
[InlineData("Medical Ethics", "Social Sciences: Communication Skills/Ethics/Patient Safety")]
|
||||
[InlineData("Integumentary System", "Skin & Subcutaneous Tissue")]
|
||||
[InlineData("Eye & Adnexa", "Nervous System & Special Senses")]
|
||||
[InlineData("Toxicology", "Immune System, Blood & Lymphoreticular System, and Multisystem Processes/Disorders")]
|
||||
public void SystemAliasesMapToOfficialCategories(string source, string expected)
|
||||
{
|
||||
Assert.Equal(expected, DashboardStore.NormalizeStep3System(source));
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
using QuestionsApi.Models;
|
||||
using QuestionsApi.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace QuestionsApi.Tests;
|
||||
|
||||
public sealed class MasteryCalculatorTests
|
||||
{
|
||||
private static readonly DateTime Now = new(2026, 7, 31, 12, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
[Fact]
|
||||
public void NoEvidenceIsUnseen()
|
||||
{
|
||||
var result = MasteryCalculator.Calculate([], [], [], new Dictionary<string, Question>(), Now);
|
||||
Assert.All(result, item => Assert.Equal(MasteryStates.Unseen, item.MasteryState));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvidenceGatesStrongAndMasteredStates()
|
||||
{
|
||||
var questions = Enumerable.Range(0, 25).ToDictionary(
|
||||
index => $"q{index}",
|
||||
index => Question($"q{index}", index < 5 ? 4 : 3));
|
||||
var attempts = questions.Keys.Select(id => Attempt(id, true, 0, Now.AddDays(-1))).ToList();
|
||||
var strong = MasteryCalculator.Calculate(attempts.Take(15), [], [], questions, Now)[6];
|
||||
var mastered = MasteryCalculator.Calculate(attempts, [], [], questions, Now)[6];
|
||||
Assert.Equal(MasteryStates.Strong, strong.MasteryState);
|
||||
Assert.Equal(MasteryStates.Mastered, mastered.MasteryState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PendingReviewBlocksMasteredAndBecomesRecommendedAction()
|
||||
{
|
||||
var questions = Enumerable.Range(0, 25).ToDictionary(
|
||||
index => $"q{index}", index => Question($"q{index}", index < 5 ? 4 : 3));
|
||||
var attempts = questions.Keys.Select(id => Attempt(id, true, 0, Now)).ToList();
|
||||
var queue = new[] { new ReviewQueueItem { QuestionExternalId = "q0", State = ReviewQueueStates.Pending } };
|
||||
var result = MasteryCalculator.Calculate(attempts, [], queue, questions, Now)[6];
|
||||
Assert.Equal(MasteryStates.Strong, result.MasteryState);
|
||||
Assert.Equal("review", result.RecommendedAction.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DelayedReviewReceivesPartialRecoveryAndOldReviewDoesNotRecoverNewCycle()
|
||||
{
|
||||
var questions = new Dictionary<string, Question> { ["q"] = Question("q", 3) };
|
||||
var first = Attempt("q", false, 0, Now.AddDays(-10));
|
||||
var second = Attempt("q", false, 1, Now.AddDays(-2));
|
||||
var reviews = new[]
|
||||
{
|
||||
new QuestionReviewAttempt { QuestionExternalId = "q", IsCorrect = true, CreatedUtc = Now.AddDays(-8) }
|
||||
};
|
||||
var result = MasteryCalculator.Calculate([first, second], reviews, [], questions, Now)[6];
|
||||
Assert.Equal(25, result.MasteryScore);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StrongMasteryIsMarkedDueWithoutDecayingState()
|
||||
{
|
||||
var questions = Enumerable.Range(0, 15).ToDictionary(index => $"q{index}", index => Question($"q{index}", 4));
|
||||
var attempts = questions.Keys.Select(id => Attempt(id, true, 0, Now.AddDays(-31))).ToList();
|
||||
var result = MasteryCalculator.Calculate(attempts, [], [], questions, Now)[6];
|
||||
Assert.Equal(MasteryStates.Strong, result.MasteryState);
|
||||
Assert.True(result.RefreshDue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SkipsDoNotCountAsEvidence()
|
||||
{
|
||||
var questions = new Dictionary<string, Question> { ["q"] = Question("q", 5) };
|
||||
var skip = Attempt("q", false, 0, Now);
|
||||
skip.IsSkip = true;
|
||||
var result = MasteryCalculator.Calculate([skip], [], [], questions, Now)[6];
|
||||
Assert.Equal(MasteryStates.Unseen, result.MasteryState);
|
||||
}
|
||||
|
||||
private static Question Question(string id, int difficulty) => new()
|
||||
{
|
||||
ExternalId = id, System = "Cardiovascular System", Step3System = "Cardiovascular System", Difficulty = difficulty
|
||||
};
|
||||
|
||||
private static QuestionAttempt Attempt(string id, bool correct, int cycle, DateTime created) => new()
|
||||
{
|
||||
QuestionExternalId = id, UserId = "user", IsCorrect = correct, BankCycle = cycle, CreatedUtc = created
|
||||
};
|
||||
}
|
||||
@@ -46,4 +46,15 @@ public sealed class QuestionStoreTests
|
||||
Assert.Equal("Reported by student", question.FlagReason);
|
||||
Assert.Equal("user-42", question.FlaggedByUserId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StudentResponseIncludesCanonicalStep3SystemForLegacyQuestion()
|
||||
{
|
||||
var response = QuestionResponse.FromModel(new Question
|
||||
{
|
||||
ExternalId = "legacy", System = "Pulmonary", Discipline = "Medicine"
|
||||
});
|
||||
|
||||
Assert.Equal("Respiratory System", response.Step3System);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,9 @@ public class QuestionsController : ControllerBase
|
||||
[Authorize(Roles = "USER,SUPER")]
|
||||
public async Task<IActionResult> Search([FromBody] QuestionSearchRequest req)
|
||||
{
|
||||
var questions = await _questions.SearchAsync(req, User.IsInRole("SUPER"));
|
||||
List<Question> questions;
|
||||
try { questions = await _questions.SearchAsync(req, User.IsInRole("SUPER")); }
|
||||
catch (ArgumentException exception) { return BadRequest(exception.Message); }
|
||||
var percentages = await _statistics.GetPercentagesByQuestionAsync(questions.Select(q => q.ExternalId));
|
||||
var questionElos = await _ratings.GetQuestionElosAsync(questions.Select(q => q.ExternalId));
|
||||
var bookmarkedIds = await _bookmarks.GetBookmarkedIdsAsync(UserId(), questions.Select(q => q.ExternalId));
|
||||
|
||||
@@ -22,9 +22,11 @@ public sealed class ReviewQueueController(
|
||||
var userId = UserId();
|
||||
if (string.IsNullOrWhiteSpace(userId)) return Unauthorized();
|
||||
await queue.EnsureUserQueueAsync(userId);
|
||||
var item = await queue.GetRandomPendingAsync(userId, req.ExcludeExternalIds);
|
||||
ReviewQueueItem? item;
|
||||
try { item = await queue.GetRandomPendingAsync(userId, req.ExcludeExternalIds, req.Step3System); }
|
||||
catch (ArgumentException exception) { return BadRequest(exception.Message); }
|
||||
if (item is null)
|
||||
return Ok(new ReviewQueueNextResponse { PendingCount = checked((int)await queue.CountPendingAsync(userId)) });
|
||||
return Ok(new ReviewQueueNextResponse { PendingCount = checked((int)await queue.CountPendingAsync(userId, req.Step3System)) });
|
||||
|
||||
var question = await questions.GetByExternalIdAsync(item.QuestionExternalId);
|
||||
if (question is null || !question.IsPublished) return NotFound();
|
||||
@@ -39,7 +41,7 @@ public sealed class ReviewQueueController(
|
||||
{
|
||||
Question = response,
|
||||
Cycle = item.Cycle,
|
||||
PendingCount = checked((int)await queue.CountPendingAsync(userId))
|
||||
PendingCount = checked((int)await queue.CountPendingAsync(userId, req.Step3System))
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -49,6 +49,32 @@ public sealed class DashboardSystemPerformance
|
||||
public int Correct { get; set; }
|
||||
public int Total { get; set; }
|
||||
public int Accuracy { get; set; }
|
||||
public int MasteryScore { get; set; }
|
||||
public string MasteryState { get; set; } = MasteryStates.Unseen;
|
||||
public int DistinctQuestions { get; set; }
|
||||
public int Difficulty4PlusSuccessful { get; set; }
|
||||
public int PendingReviews { get; set; }
|
||||
public DateTime? LastEvidenceUtc { get; set; }
|
||||
public bool RefreshDue { get; set; }
|
||||
public DashboardMasteryAction RecommendedAction { get; set; } = new();
|
||||
}
|
||||
|
||||
public static class MasteryStates
|
||||
{
|
||||
public const string Unseen = "unseen";
|
||||
public const string Learning = "learning";
|
||||
public const string Practicing = "practicing";
|
||||
public const string Strong = "strong";
|
||||
public const string Mastered = "mastered";
|
||||
}
|
||||
|
||||
public sealed class DashboardMasteryAction
|
||||
{
|
||||
public string Type { get; set; } = "practice";
|
||||
public string Label { get; set; } = "Start learning";
|
||||
public int QuestionCount { get; set; } = 10;
|
||||
public int? DifficultyMin { get; set; }
|
||||
public int? DifficultyMax { get; set; }
|
||||
}
|
||||
|
||||
public sealed class DashboardActivityDay
|
||||
|
||||
@@ -23,6 +23,7 @@ public class Question
|
||||
public Dictionary<string, int> ChoiceSelectionPercentages { get; set; } = [];
|
||||
public string Discipline { get; set; } = "";
|
||||
public string System { get; set; } = "";
|
||||
public string Step3System { get; set; } = "";
|
||||
public string PerformanceCategory { get; set; } = "";
|
||||
public int Difficulty { get; set; } = 1;
|
||||
public List<QuestionLab> Labs { get; set; } = [];
|
||||
|
||||
@@ -3,6 +3,7 @@ namespace QuestionsApi.Models;
|
||||
public class QuestionSearchRequest
|
||||
{
|
||||
public string? System { get; set; }
|
||||
public string? Step3System { get; set; }
|
||||
public string? Discipline { get; set; }
|
||||
public List<string> Tags { get; set; } = [];
|
||||
public int? DifficultyMin { get; set; }
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using QuestionsApi.Services;
|
||||
|
||||
namespace QuestionsApi.Models;
|
||||
|
||||
public class QuestionResponse
|
||||
@@ -18,6 +20,7 @@ public class QuestionResponse
|
||||
public string ChoiceSelectionRateBasis { get; set; } = "presented";
|
||||
public string Discipline { get; set; } = "";
|
||||
public string System { get; set; } = "";
|
||||
public string Step3System { get; set; } = "";
|
||||
public string PerformanceCategory { get; set; } = "";
|
||||
public int Difficulty { get; set; }
|
||||
public int QuestionElo { get; set; } = 1000;
|
||||
@@ -59,6 +62,7 @@ public class QuestionResponse
|
||||
.ToDictionary(),
|
||||
Discipline = question.Discipline,
|
||||
System = question.System,
|
||||
Step3System = Step3SystemCatalog.Normalize(question) ?? "",
|
||||
PerformanceCategory = RatingCategory.Normalize(question.PerformanceCategory, question.System, question.Discipline),
|
||||
Difficulty = question.Difficulty,
|
||||
QuestionElo = questionElo,
|
||||
|
||||
@@ -55,6 +55,7 @@ public sealed class ReviewQueueNextRequest
|
||||
{
|
||||
public List<string> ExcludeExternalIds { get; set; } = [];
|
||||
public bool SupportsChoicePools { get; set; } = true;
|
||||
public string? Step3System { get; set; }
|
||||
}
|
||||
|
||||
public sealed class ReviewQueueNextResponse
|
||||
|
||||
@@ -66,6 +66,17 @@ if (args.FirstOrDefault() == "migrate-question-attempt-cycles")
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.FirstOrDefault() == "backfill-step3-systems")
|
||||
{
|
||||
var migrationConfiguration = new ConfigurationBuilder()
|
||||
.AddJsonFile("appsettings.json", optional: true)
|
||||
.AddEnvironmentVariables()
|
||||
.Build();
|
||||
Environment.ExitCode = await Step3SystemBackfillCommand.RunAsync(
|
||||
migrationConfiguration, args.Skip(1).ToArray(), Console.Out);
|
||||
return;
|
||||
}
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.Configure<ForwardedHeadersOptions>(options =>
|
||||
|
||||
@@ -22,23 +22,7 @@ public sealed class DashboardStore
|
||||
private readonly CcsRatingStore _ccsRatingStore;
|
||||
private readonly IMongoCollection<UserRating> _ratings;
|
||||
|
||||
public static readonly string[] Step3Systems =
|
||||
[
|
||||
"Human Development",
|
||||
"Immune System, Blood & Lymphoreticular System, and Multisystem Processes/Disorders",
|
||||
"Behavioral Health",
|
||||
"Nervous System & Special Senses",
|
||||
"Skin & Subcutaneous Tissue",
|
||||
"Musculoskeletal System",
|
||||
"Cardiovascular System",
|
||||
"Respiratory System",
|
||||
"Gastrointestinal System",
|
||||
"Renal/Urinary & Male Reproductive Systems",
|
||||
"Pregnancy/Childbirth & Female Reproductive System & Breast",
|
||||
"Endocrine System",
|
||||
"Biostatistics & Epidemiology/Population Health & Interpretation of the Medical Literature",
|
||||
"Social Sciences: Communication Skills/Ethics/Patient Safety"
|
||||
];
|
||||
public static readonly string[] Step3Systems = Step3SystemCatalog.All;
|
||||
|
||||
public DashboardStore(
|
||||
MongoDatabaseProvider mongo,
|
||||
@@ -69,6 +53,7 @@ public sealed class DashboardStore
|
||||
|
||||
public async Task<DashboardResponse> GetAsync(string userId, DateTime nowUtc)
|
||||
{
|
||||
await _reviewQueue.EnsureUserQueueAsync(userId);
|
||||
var today = nowUtc.Date;
|
||||
var trendStart = today.AddDays(-29);
|
||||
var weekStart = today.AddDays(-6);
|
||||
@@ -86,22 +71,25 @@ public sealed class DashboardStore
|
||||
var bookmarkCountTask = _bookmarks.CountDocumentsAsync(value => value.UserId == userId);
|
||||
var notepadTask = _notes.Find(value => value.UserId == userId).FirstOrDefaultAsync();
|
||||
var reviewAttemptsTask = _reviewQueue.GetReviewsSinceAsync(userId, weekStart);
|
||||
var allReviewAttemptsTask = _reviewQueue.GetUserReviewsAsync(userId);
|
||||
var reviewItemsTask = _reviewQueue.GetUserItemsAsync(userId);
|
||||
var flashcardCountsTask = _flashcards.GetDashboardCountsAsync(userId, nowUtc, weekStart);
|
||||
var flashcardReviewsTask = _flashcardSchedules.GetReviewsSinceAsync(userId, weekStart);
|
||||
var dynamicReviewsTask = _dynamicQuestions.DueReviewsAsync(userId, nowUtc);
|
||||
await Task.WhenAll(attemptsTask, ratingTask, ccsRatingTask, ccsAttemptsTask, ccsAllAttemptsTask, completedCcsCountTask,
|
||||
simulationsTask, completedSimulationsTask, bookmarkCountTask, notepadTask, reviewAttemptsTask, flashcardCountsTask,
|
||||
flashcardReviewsTask, dynamicReviewsTask);
|
||||
flashcardReviewsTask, dynamicReviewsTask, allReviewAttemptsTask, reviewItemsTask);
|
||||
|
||||
var attempts = attemptsTask.Result;
|
||||
var answeredAttempts = attempts.Where(value => !value.IsSkip).ToList();
|
||||
var recentAnswered = answeredAttempts.OrderByDescending(value => value.CreatedUtc).Take(100).ToList();
|
||||
var previousHundred = answeredAttempts.OrderByDescending(value => value.CreatedUtc).Skip(100).Take(100).ToList();
|
||||
var questionMap = await _questions.GetByExternalIdsAsync(attempts.Select(value => value.QuestionExternalId));
|
||||
var questionMap = await _questions.GetByExternalIdsAsync(attempts.Select(value => value.QuestionExternalId)
|
||||
.Concat(reviewItemsTask.Result.Select(value => value.QuestionExternalId))
|
||||
.Concat(allReviewAttemptsTask.Result.Select(value => value.QuestionExternalId)));
|
||||
var ccsAttempts = ccsAttemptsTask.Result;
|
||||
var simulations = simulationsTask.Result;
|
||||
var completedCcs = ccsAttempts.Where(value => value.State == CcsAttemptStates.Completed && value.CompletedUtc.HasValue).ToList();
|
||||
await _reviewQueue.EnsureUserQueueAsync(userId);
|
||||
var pendingReviewCount = await _reviewQueue.CountPendingAsync(userId);
|
||||
|
||||
var qbankElo = ratingTask.Result.OverallElo;
|
||||
@@ -148,7 +136,8 @@ public sealed class DashboardStore
|
||||
};
|
||||
|
||||
response.PerformanceTrend = performanceTrend;
|
||||
response.Systems = BuildSystemPerformance(answeredAttempts, questionMap);
|
||||
response.Systems = MasteryCalculator.Calculate(
|
||||
attempts, allReviewAttemptsTask.Result, reviewItemsTask.Result, questionMap, nowUtc);
|
||||
|
||||
response.WeeklyActivity = Enumerable.Range(0, 7).Select(offset =>
|
||||
{
|
||||
@@ -303,59 +292,10 @@ public sealed class DashboardStore
|
||||
}
|
||||
|
||||
public static string? NormalizeStep3System(Question question)
|
||||
{
|
||||
foreach (var candidate in new[] { question.System, question.PerformanceCategory, question.Discipline })
|
||||
{
|
||||
var normalized = NormalizeStep3System(candidate);
|
||||
if (normalized is not null)
|
||||
return normalized;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
=> Step3SystemCatalog.Normalize(question);
|
||||
|
||||
public static string? NormalizeStep3System(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
return null;
|
||||
var exact = Step3Systems.FirstOrDefault(
|
||||
system => string.Equals(system, value.Trim(), StringComparison.OrdinalIgnoreCase));
|
||||
if (exact is not null)
|
||||
return exact;
|
||||
|
||||
var lower = value.Trim().ToLowerInvariant();
|
||||
if (ContainsAny(lower, "human development", "well patient", "preventive", "health maintenance", "geriatr", "age-related"))
|
||||
return Step3Systems[0];
|
||||
if (ContainsAny(lower, "social science", "communication", "ethic", "patient safety", "professionalism", "systems-based"))
|
||||
return Step3Systems[13];
|
||||
if (ContainsAny(lower, "pregnan", "obstetric", "gynec", "female reproductive", "breast", "puerper"))
|
||||
return Step3Systems[10];
|
||||
if (ContainsAny(lower, "renal", "urinary", "male reproductive", "urolog", "nephro"))
|
||||
return Step3Systems[9];
|
||||
if (ContainsAny(lower, "biostat", "epidemi", "population health", "medical literature"))
|
||||
return Step3Systems[12];
|
||||
if (ContainsAny(lower, "immune", "immun", "blood", "hemat", "lymph", "multisystem", "infectious"))
|
||||
return Step3Systems[1];
|
||||
if (ContainsAny(lower, "behavior", "psychi", "mental health"))
|
||||
return Step3Systems[2];
|
||||
if (ContainsAny(lower, "nervous", "neuro", "special sense", "ophthalm", "otolaryng", " ent"))
|
||||
return Step3Systems[3];
|
||||
if (ContainsAny(lower, "skin", "derm", "subcutaneous"))
|
||||
return Step3Systems[4];
|
||||
if (ContainsAny(lower, "musculo", "orthop", "rheumat"))
|
||||
return Step3Systems[5];
|
||||
if (ContainsAny(lower, "cardio", "circulatory"))
|
||||
return Step3Systems[6];
|
||||
if (ContainsAny(lower, "resp", "pulmon"))
|
||||
return Step3Systems[7];
|
||||
if (ContainsAny(lower, "gastro", "digest", "hepato"))
|
||||
return Step3Systems[8];
|
||||
if (ContainsAny(lower, "endocr", "metabolic"))
|
||||
return Step3Systems[11];
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool ContainsAny(string value, params string[] fragments) =>
|
||||
fragments.Any(value.Contains);
|
||||
=> Step3SystemCatalog.Normalize(value);
|
||||
|
||||
private async Task<List<DashboardRecentActivity>> BuildRecentActivityAsync(
|
||||
List<QuestionAttempt> attempts,
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
using QuestionsApi.Models;
|
||||
|
||||
namespace QuestionsApi.Services;
|
||||
|
||||
public static class MasteryCalculator
|
||||
{
|
||||
private static readonly IReadOnlyDictionary<int, double> DifficultyWeights = new Dictionary<int, double>
|
||||
{
|
||||
[1] = 0.6, [2] = 0.8, [3] = 1.0, [4] = 1.2, [5] = 1.4
|
||||
};
|
||||
|
||||
public static List<DashboardSystemPerformance> Calculate(
|
||||
IEnumerable<QuestionAttempt> attempts,
|
||||
IEnumerable<QuestionReviewAttempt> reviews,
|
||||
IEnumerable<ReviewQueueItem> queueItems,
|
||||
IReadOnlyDictionary<string, Question> questions,
|
||||
DateTime nowUtc)
|
||||
{
|
||||
var mappedAttempts = attempts.Where(attempt => questions.ContainsKey(attempt.QuestionExternalId)).ToList();
|
||||
var answered = mappedAttempts.Where(attempt => !attempt.IsSkip).ToList();
|
||||
var maxCycle = mappedAttempts.Count == 0 ? 0 : mappedAttempts.Max(attempt => attempt.BankCycle);
|
||||
var reviewList = reviews.OrderBy(review => review.CreatedUtc).ToList();
|
||||
var queueList = queueItems.ToList();
|
||||
var results = new List<DashboardSystemPerformance>();
|
||||
foreach (var system in Step3SystemCatalog.All)
|
||||
{
|
||||
var systemAttempts = answered.Where(attempt =>
|
||||
Step3SystemCatalog.Normalize(questions[attempt.QuestionExternalId]) == system).ToList();
|
||||
var ids = systemAttempts.Select(attempt => attempt.QuestionExternalId).Distinct(StringComparer.Ordinal).ToHashSet(StringComparer.Ordinal);
|
||||
var systemReviews = reviewList.Where(review => ids.Contains(review.QuestionExternalId)).ToList();
|
||||
var pending = queueList.Count(item => item.State == ReviewQueueStates.Pending
|
||||
&& questions.TryGetValue(item.QuestionExternalId, out var question)
|
||||
&& Step3SystemCatalog.Normalize(question) == system);
|
||||
double earned = 0;
|
||||
double possible = 0;
|
||||
var advancedSuccesses = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (var attempt in systemAttempts)
|
||||
{
|
||||
var question = questions[attempt.QuestionExternalId];
|
||||
var weight = DifficultyWeights.GetValueOrDefault(Math.Clamp(question.Difficulty, 1, 5), 1.0)
|
||||
* Math.Pow(0.5, Math.Max(0, maxCycle - attempt.BankCycle));
|
||||
var outcome = attempt.IsCorrect ? 1.0 : RecoveryOutcome(attempt, systemAttempts, systemReviews);
|
||||
earned += weight * outcome;
|
||||
possible += weight;
|
||||
if (question.Difficulty >= 4 && outcome >= 0.75) advancedSuccesses.Add(question.ExternalId);
|
||||
}
|
||||
var score = possible == 0 ? 0 : (int)Math.Round(earned / possible * 100, MidpointRounding.AwayFromZero);
|
||||
var state = State(ids.Count, score, advancedSuccesses.Count, pending);
|
||||
var lastEvidence = systemAttempts.Select(value => (DateTime?)value.CreatedUtc)
|
||||
.Concat(systemReviews.Select(value => (DateTime?)value.CreatedUtc)).Max();
|
||||
var refreshDue = state is MasteryStates.Strong or MasteryStates.Mastered
|
||||
&& lastEvidence.HasValue && lastEvidence.Value < nowUtc.AddDays(-30);
|
||||
results.Add(new DashboardSystemPerformance
|
||||
{
|
||||
System = system,
|
||||
Correct = systemAttempts.Count(value => value.IsCorrect),
|
||||
Total = systemAttempts.Count,
|
||||
Accuracy = DashboardStore.Percentage(systemAttempts.Count(value => value.IsCorrect), systemAttempts.Count),
|
||||
MasteryScore = score,
|
||||
MasteryState = state,
|
||||
DistinctQuestions = ids.Count,
|
||||
Difficulty4PlusSuccessful = advancedSuccesses.Count,
|
||||
PendingReviews = pending,
|
||||
LastEvidenceUtc = lastEvidence,
|
||||
RefreshDue = refreshDue,
|
||||
RecommendedAction = Action(state, pending, refreshDue)
|
||||
});
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
private static double RecoveryOutcome(
|
||||
QuestionAttempt attempt,
|
||||
IReadOnlyCollection<QuestionAttempt> attempts,
|
||||
IReadOnlyCollection<QuestionReviewAttempt> reviews)
|
||||
{
|
||||
var nextAttempt = attempts.Where(value => value.QuestionExternalId == attempt.QuestionExternalId
|
||||
&& value.CreatedUtc > attempt.CreatedUtc)
|
||||
.OrderBy(value => value.CreatedUtc).FirstOrDefault();
|
||||
var recovery = reviews.Where(value => value.QuestionExternalId == attempt.QuestionExternalId
|
||||
&& value.IsCorrect && value.CreatedUtc > attempt.CreatedUtc
|
||||
&& (nextAttempt is null || value.CreatedUtc < nextAttempt.CreatedUtc))
|
||||
.OrderBy(value => value.CreatedUtc).FirstOrDefault();
|
||||
if (recovery is null) return 0;
|
||||
return recovery.CreatedUtc - attempt.CreatedUtc >= TimeSpan.FromHours(24) ? 0.75 : 0.5;
|
||||
}
|
||||
|
||||
private static string State(int distinct, int score, int advanced, int pending)
|
||||
{
|
||||
if (distinct == 0) return MasteryStates.Unseen;
|
||||
if (distinct < 5) return MasteryStates.Learning;
|
||||
if (distinct >= 25 && score >= 85 && advanced >= 5 && pending == 0) return MasteryStates.Mastered;
|
||||
if (distinct >= 15 && score >= 75) return MasteryStates.Strong;
|
||||
return MasteryStates.Practicing;
|
||||
}
|
||||
|
||||
private static DashboardMasteryAction Action(string state, int pending, bool refreshDue)
|
||||
{
|
||||
if (pending > 0) return new() { Type = "review", Label = "Review missed concepts", QuestionCount = pending };
|
||||
return state switch
|
||||
{
|
||||
MasteryStates.Unseen or MasteryStates.Learning => new() { Type = "practice", Label = "Start learning", QuestionCount = 10 },
|
||||
MasteryStates.Practicing => new() { Type = "practice", Label = "Build mastery", QuestionCount = 10, DifficultyMin = 3, DifficultyMax = 4 },
|
||||
MasteryStates.Strong => new() { Type = "practice", Label = "Challenge this system", QuestionCount = 10, DifficultyMin = 4, DifficultyMax = 5 },
|
||||
MasteryStates.Mastered when refreshDue => new() { Type = "practice", Label = "Refresh mastery", QuestionCount = 5, DifficultyMin = 4, DifficultyMax = 5 },
|
||||
_ => new() { Type = "practice", Label = "Maintain mastery", QuestionCount = 5, DifficultyMin = 4, DifficultyMax = 5 }
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,10 @@ public class QuestionStore
|
||||
_col.Indexes.CreateOne(new CreateIndexModel<Question>(systemIndex));
|
||||
var tagsIndex = Builders<Question>.IndexKeys.Ascending(q => q.Tags);
|
||||
_col.Indexes.CreateOne(new CreateIndexModel<Question>(tagsIndex));
|
||||
var step3SystemIndex = Builders<Question>.IndexKeys.Ascending(q => q.Step3System)
|
||||
.Ascending(q => q.IsPublished).Ascending(q => q.FlaggedForReviewUtc).Ascending(q => q.Difficulty);
|
||||
_col.Indexes.CreateOne(new CreateIndexModel<Question>(step3SystemIndex,
|
||||
new CreateIndexOptions { Name = "step3_system_visibility_difficulty" }));
|
||||
}
|
||||
|
||||
public async Task<List<Question>> SearchAsync(QuestionSearchRequest req, bool includeUnpublished)
|
||||
@@ -55,10 +59,18 @@ public class QuestionStore
|
||||
|
||||
var filter = filters.Count == 0 ? builder.Empty : builder.And(filters);
|
||||
var limit = Math.Clamp(req.Limit, 1, 80);
|
||||
return await _col.Aggregate()
|
||||
.Match(filter)
|
||||
.Sample(limit)
|
||||
.ToListAsync();
|
||||
if (string.IsNullOrWhiteSpace(req.Step3System))
|
||||
return await _col.Aggregate().Match(filter).Sample(limit).ToListAsync();
|
||||
|
||||
var requested = Step3SystemCatalog.Normalize(req.Step3System)
|
||||
?? throw new ArgumentException("step3System must be an official Step 3 system");
|
||||
// Include legacy records until the idempotent Step3System backfill has run.
|
||||
var systemFilter = builder.Eq(question => question.Step3System, requested)
|
||||
| builder.Exists(nameof(Question.Step3System), false)
|
||||
| builder.Eq(question => question.Step3System, "");
|
||||
var candidates = await _col.Find(filter & systemFilter).ToListAsync();
|
||||
return candidates.Where(question => Step3SystemCatalog.Normalize(question) == requested)
|
||||
.OrderBy(_ => Random.Shared.Next()).Take(limit).ToList();
|
||||
}
|
||||
|
||||
public async Task<Question?> GetByIdAsync(string id) =>
|
||||
@@ -280,6 +292,11 @@ public class QuestionStore
|
||||
ChoiceSelectionPercentages = req.ChoiceSelectionPercentages,
|
||||
Discipline = req.Discipline.Trim(),
|
||||
System = req.System.Trim(),
|
||||
Step3System = Step3SystemCatalog.Derive(new Question
|
||||
{
|
||||
ExternalId = req.ExternalId.Trim(), System = req.System.Trim(),
|
||||
PerformanceCategory = req.PerformanceCategory.Trim(), Discipline = req.Discipline.Trim()
|
||||
}) ?? "",
|
||||
PerformanceCategory = RatingCategory.Normalize(req.PerformanceCategory, req.System, req.Discipline),
|
||||
Difficulty = req.Difficulty,
|
||||
Labs = req.Labs,
|
||||
|
||||
@@ -46,15 +46,20 @@ public sealed class ReviewQueueStore
|
||||
var update = Builders<ReviewQueueItem>.Update
|
||||
.SetOnInsert(value => value.UserId, attempt.UserId)
|
||||
.SetOnInsert(value => value.QuestionExternalId, attempt.QuestionExternalId)
|
||||
.SetOnInsert(value => value.SourceAttemptId, attempt.Id ?? "")
|
||||
.SetOnInsert(value => value.State, ReviewQueueStates.Pending)
|
||||
.SetOnInsert(value => value.Cycle, 1)
|
||||
.SetOnInsert(value => value.CreatedUtc, attempt.CreatedUtc)
|
||||
.SetOnInsert(value => value.UpdatedUtc, now);
|
||||
.Set(value => value.SourceAttemptId, attempt.Id ?? "")
|
||||
.Set(value => value.State, ReviewQueueStates.Pending)
|
||||
.Set(value => value.MasteredUtc, null)
|
||||
.Set(value => value.UpdatedUtc, now);
|
||||
var filter = Builders<ReviewQueueItem>.Filter.Eq(value => value.UserId, attempt.UserId)
|
||||
& Builders<ReviewQueueItem>.Filter.Eq(value => value.QuestionExternalId, attempt.QuestionExternalId)
|
||||
& (Builders<ReviewQueueItem>.Filter.Ne(value => value.State, ReviewQueueStates.Mastered)
|
||||
| Builders<ReviewQueueItem>.Filter.Ne(value => value.SourceAttemptId, attempt.Id ?? ""));
|
||||
try
|
||||
{
|
||||
await _items.UpdateOneAsync(
|
||||
value => value.UserId == attempt.UserId && value.QuestionExternalId == attempt.QuestionExternalId,
|
||||
filter,
|
||||
update,
|
||||
new UpdateOptions { IsUpsert = true });
|
||||
}
|
||||
@@ -66,26 +71,36 @@ public sealed class ReviewQueueStore
|
||||
|
||||
public async Task EnsureUserQueueAsync(string userId)
|
||||
{
|
||||
var checkpoint = await _backfills.Find(value => value.UserId == userId).FirstOrDefaultAsync();
|
||||
var scanThroughUtc = DateTime.UtcNow;
|
||||
var filter = Builders<QuestionAttempt>.Filter.Where(value => value.UserId == userId && !value.IsCorrect && !value.IsSkip);
|
||||
if (checkpoint is not null)
|
||||
filter &= Builders<QuestionAttempt>.Filter.Gte(value => value.CreatedUtc, checkpoint.CompletedUtc);
|
||||
filter &= Builders<QuestionAttempt>.Filter.Lte(value => value.CreatedUtc, scanThroughUtc);
|
||||
var misses = await _firstAttempts.Find(filter).ToListAsync();
|
||||
var allMisses = await _firstAttempts.Find(filter).ToListAsync();
|
||||
var existingItems = (await _items.Find(value => value.UserId == userId).ToListAsync())
|
||||
.ToDictionary(value => value.QuestionExternalId, StringComparer.Ordinal);
|
||||
var misses = allMisses.GroupBy(value => value.QuestionExternalId, StringComparer.Ordinal)
|
||||
.Select(group => group.OrderByDescending(value => value.CreatedUtc).First())
|
||||
.Where(attempt => !existingItems.TryGetValue(attempt.QuestionExternalId, out var item)
|
||||
|| (item.State == ReviewQueueStates.Mastered
|
||||
&& item.SourceAttemptId != (attempt.Id ?? "")
|
||||
&& (!item.MasteredUtc.HasValue || attempt.CreatedUtc > item.MasteredUtc.Value)))
|
||||
.ToList();
|
||||
if (misses.Count > 0)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var writes = misses.Select(attempt => new UpdateOneModel<ReviewQueueItem>(
|
||||
Builders<ReviewQueueItem>.Filter.Where(value => value.UserId == userId && value.QuestionExternalId == attempt.QuestionExternalId),
|
||||
Builders<ReviewQueueItem>.Filter.Eq(value => value.UserId, userId)
|
||||
& Builders<ReviewQueueItem>.Filter.Eq(value => value.QuestionExternalId, attempt.QuestionExternalId)
|
||||
& (Builders<ReviewQueueItem>.Filter.Ne(value => value.State, ReviewQueueStates.Mastered)
|
||||
| Builders<ReviewQueueItem>.Filter.Ne(value => value.SourceAttemptId, attempt.Id ?? "")),
|
||||
Builders<ReviewQueueItem>.Update
|
||||
.SetOnInsert(value => value.UserId, userId)
|
||||
.SetOnInsert(value => value.QuestionExternalId, attempt.QuestionExternalId)
|
||||
.SetOnInsert(value => value.SourceAttemptId, attempt.Id ?? "")
|
||||
.SetOnInsert(value => value.State, ReviewQueueStates.Pending)
|
||||
.SetOnInsert(value => value.Cycle, 1)
|
||||
.SetOnInsert(value => value.CreatedUtc, attempt.CreatedUtc)
|
||||
.SetOnInsert(value => value.UpdatedUtc, now))
|
||||
.Set(value => value.SourceAttemptId, attempt.Id ?? "")
|
||||
.Set(value => value.State, ReviewQueueStates.Pending)
|
||||
.Set(value => value.MasteredUtc, null)
|
||||
.Set(value => value.UpdatedUtc, now))
|
||||
{ IsUpsert = true }).ToList();
|
||||
try
|
||||
{
|
||||
@@ -115,27 +130,50 @@ public sealed class ReviewQueueStore
|
||||
public Task<long> CountPendingAsync(string userId) =>
|
||||
_items.CountDocumentsAsync(value => value.UserId == userId && value.State == ReviewQueueStates.Pending);
|
||||
|
||||
public async Task<ReviewQueueItem?> GetRandomPendingAsync(string userId, IEnumerable<string> excludedIds)
|
||||
public async Task<long> CountPendingAsync(string userId, string? step3System)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(step3System)) return await CountPendingAsync(userId);
|
||||
var requested = Step3SystemCatalog.Normalize(step3System)
|
||||
?? throw new ArgumentException("step3System must be an official Step 3 system");
|
||||
var pending = await _items.Find(value => value.UserId == userId && value.State == ReviewQueueStates.Pending).ToListAsync();
|
||||
var count = 0L;
|
||||
foreach (var item in pending)
|
||||
{
|
||||
var question = await _questions.GetByExternalIdAsync(item.QuestionExternalId);
|
||||
if (question is not null && Step3SystemCatalog.Normalize(question) == requested) count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
public Task<List<ReviewQueueItem>> GetUserItemsAsync(string userId) =>
|
||||
_items.Find(value => value.UserId == userId).ToListAsync();
|
||||
|
||||
public Task<List<QuestionReviewAttempt>> GetUserReviewsAsync(string userId) =>
|
||||
_reviews.Find(value => value.UserId == userId).ToListAsync();
|
||||
|
||||
public async Task<ReviewQueueItem?> GetRandomPendingAsync(
|
||||
string userId, IEnumerable<string> excludedIds, string? step3System = null)
|
||||
{
|
||||
var excluded = excludedIds.Where(value => !string.IsNullOrWhiteSpace(value)).Distinct(StringComparer.Ordinal).ToList();
|
||||
for (var pass = 0; pass < 5; pass++)
|
||||
var requestedSystem = string.IsNullOrWhiteSpace(step3System) ? null : Step3SystemCatalog.Normalize(step3System);
|
||||
if (!string.IsNullOrWhiteSpace(step3System) && requestedSystem is null)
|
||||
throw new ArgumentException("step3System must be an official Step 3 system");
|
||||
var filter = Builders<ReviewQueueItem>.Filter.Eq(value => value.UserId, userId)
|
||||
& Builders<ReviewQueueItem>.Filter.Eq(value => value.State, ReviewQueueStates.Pending);
|
||||
if (excluded.Count > 0) filter &= Builders<ReviewQueueItem>.Filter.Nin(value => value.QuestionExternalId, excluded);
|
||||
var candidates = await _items.Find(filter).ToListAsync();
|
||||
foreach (var candidate in candidates.OrderBy(_ => Random.Shared.Next()))
|
||||
{
|
||||
var filter = Builders<ReviewQueueItem>.Filter.Eq(value => value.UserId, userId)
|
||||
& Builders<ReviewQueueItem>.Filter.Eq(value => value.State, ReviewQueueStates.Pending);
|
||||
if (excluded.Count > 0)
|
||||
filter &= Builders<ReviewQueueItem>.Filter.Nin(value => value.QuestionExternalId, excluded);
|
||||
var candidates = await _items.Aggregate().Match(filter).Sample(20).ToListAsync();
|
||||
if (candidates.Count == 0) return null;
|
||||
foreach (var candidate in candidates)
|
||||
var question = await _questions.GetByExternalIdAsync(candidate.QuestionExternalId);
|
||||
if (question is { IsPublished: true, FlaggedForReviewUtc: null })
|
||||
{
|
||||
var question = await _questions.GetByExternalIdAsync(candidate.QuestionExternalId);
|
||||
if (question is { IsPublished: true, FlaggedForReviewUtc: null }) return candidate;
|
||||
await _items.UpdateOneAsync(
|
||||
value => value.Id == candidate.Id && value.State == ReviewQueueStates.Pending,
|
||||
Builders<ReviewQueueItem>.Update
|
||||
.Set(value => value.State, ReviewQueueStates.Unavailable)
|
||||
.Set(value => value.UpdatedUtc, DateTime.UtcNow));
|
||||
if (requestedSystem is null || Step3SystemCatalog.Normalize(question) == requestedSystem) return candidate;
|
||||
continue;
|
||||
}
|
||||
await _items.UpdateOneAsync(
|
||||
value => value.Id == candidate.Id && value.State == ReviewQueueStates.Pending,
|
||||
Builders<ReviewQueueItem>.Update.Set(value => value.State, ReviewQueueStates.Unavailable)
|
||||
.Set(value => value.UpdatedUtc, DateTime.UtcNow));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Driver;
|
||||
using QuestionsApi.Models;
|
||||
|
||||
namespace QuestionsApi.Services;
|
||||
|
||||
public static class Step3SystemBackfillCommand
|
||||
{
|
||||
public const string Confirmation = "BACKFILL_STEP3_SYSTEMS";
|
||||
|
||||
public static async Task<int> RunAsync(IConfiguration configuration, string[] args, TextWriter output)
|
||||
{
|
||||
var apply = args.Contains("--apply", StringComparer.Ordinal);
|
||||
var confirmIndex = Array.IndexOf(args, "--confirm");
|
||||
var confirmation = confirmIndex >= 0 && confirmIndex + 1 < args.Length ? args[confirmIndex + 1] : "";
|
||||
var database = new MongoDatabaseProvider(configuration).Database;
|
||||
var questions = database.GetCollection<Question>("Questions");
|
||||
var all = await questions.Find(FilterDefinition<Question>.Empty).ToListAsync();
|
||||
var classified = all.Select(question => (Question: question, System: Step3SystemCatalog.Derive(question) ?? "")).ToList();
|
||||
var changes = classified.Where(item => item.System.Length > 0 && item.Question.Step3System != item.System).ToList();
|
||||
var unmapped = classified.Count(item => item.System.Length == 0);
|
||||
await output.WriteLineAsync($"Questions requiring Step3System backfill: {changes.Count}; unmapped: {unmapped}.");
|
||||
foreach (var group in classified.Where(item => item.System.Length == 0)
|
||||
.GroupBy(item => $"{item.Question.System} | {item.Question.PerformanceCategory} | {item.Question.Discipline}")
|
||||
.OrderByDescending(group => group.Count()).ThenBy(group => group.Key, StringComparer.Ordinal))
|
||||
await output.WriteLineAsync($"Unmapped ({group.Count()}): {group.Key}; ids: {string.Join(", ", group.Select(item => item.Question.ExternalId).Take(10))}");
|
||||
if (!apply)
|
||||
{
|
||||
await output.WriteLineAsync($"Dry run only. Re-run with --apply --confirm {Confirmation}.");
|
||||
return 0;
|
||||
}
|
||||
if (confirmation != Confirmation)
|
||||
{
|
||||
await output.WriteLineAsync($"Refusing migration: --confirm {Confirmation} is required.");
|
||||
return 2;
|
||||
}
|
||||
if (changes.Count > 0)
|
||||
{
|
||||
var writes = changes.Select(item => new UpdateOneModel<Question>(
|
||||
Builders<Question>.Filter.Eq(question => question.Id, item.Question.Id),
|
||||
Builders<Question>.Update.Set(question => question.Step3System, item.System))).ToList();
|
||||
await questions.BulkWriteAsync(writes, new BulkWriteOptions { IsOrdered = false });
|
||||
}
|
||||
var remaining = await questions.CountDocumentsAsync(new BsonDocumentFilterDefinition<Question>(
|
||||
new BsonDocument("$or", new BsonArray
|
||||
{
|
||||
new BsonDocument(nameof(Question.Step3System), new BsonDocument("$exists", false)),
|
||||
new BsonDocument(nameof(Question.Step3System), "")
|
||||
})));
|
||||
await output.WriteLineAsync($"Backfilled {changes.Count}; unmapped or empty Step3System: {remaining}.");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
using QuestionsApi.Models;
|
||||
|
||||
namespace QuestionsApi.Services;
|
||||
|
||||
public static class Step3SystemCatalog
|
||||
{
|
||||
private static readonly IReadOnlyDictionary<string, string> Overrides = new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["d5-expansion-pediatrics-urea-cycle-hyperammonemia-001"] = "Endocrine System",
|
||||
["d5-expansion-pediatrics-neonatal-hsv-disseminated-001"] = "Immune System, Blood & Lymphoreticular System, and Multisystem Processes/Disorders",
|
||||
["d5-expansion-pediatrics-cdh-delivery-stabilization-001"] = "Respiratory System",
|
||||
["d5-expansion-pediatrics-hie-therapeutic-hypothermia-001"] = "Nervous System & Special Senses",
|
||||
["d5-expansion-pediatrics-fnait-intracranial-bleeding-001"] = "Immune System, Blood & Lymphoreticular System, and Multisystem Processes/Disorders",
|
||||
["nbme6-s3-i029-infant-hyperchloremic-acidosis-growth-risk"] = "Renal/Urinary & Male Reproductive Systems",
|
||||
["form7-s1-i042-genitourinary-syndrome-menopause"] = "Pregnancy/Childbirth & Female Reproductive System & Breast",
|
||||
["free137-i019-inguinal-node-vulvar-source"] = "Pregnancy/Childbirth & Female Reproductive System & Breast",
|
||||
["free137-i061-klinefelter-confirmation"] = "Renal/Urinary & Male Reproductive Systems",
|
||||
["form7-s1-i009-cremasteric-reflex"] = "Renal/Urinary & Male Reproductive Systems",
|
||||
["form7-s2-i015-acrosome-head"] = "Renal/Urinary & Male Reproductive Systems",
|
||||
["nbme6-s1-i033-prostate-primary-osteoblastic-metastases"] = "Renal/Urinary & Male Reproductive Systems",
|
||||
["nbme6-s3-i043-lytic-lesion-hematuria-staging"] = "Renal/Urinary & Male Reproductive Systems"
|
||||
};
|
||||
public static readonly string[] All =
|
||||
[
|
||||
"Human Development",
|
||||
"Immune System, Blood & Lymphoreticular System, and Multisystem Processes/Disorders",
|
||||
"Behavioral Health",
|
||||
"Nervous System & Special Senses",
|
||||
"Skin & Subcutaneous Tissue",
|
||||
"Musculoskeletal System",
|
||||
"Cardiovascular System",
|
||||
"Respiratory System",
|
||||
"Gastrointestinal System",
|
||||
"Renal/Urinary & Male Reproductive Systems",
|
||||
"Pregnancy/Childbirth & Female Reproductive System & Breast",
|
||||
"Endocrine System",
|
||||
"Biostatistics & Epidemiology/Population Health & Interpretation of the Medical Literature",
|
||||
"Social Sciences: Communication Skills/Ethics/Patient Safety"
|
||||
];
|
||||
|
||||
public static string? Normalize(Question question) =>
|
||||
Normalize(question.Step3System)
|
||||
?? Derive(question);
|
||||
|
||||
public static string? Derive(Question question) =>
|
||||
Overrides.GetValueOrDefault(question.ExternalId)
|
||||
?? Normalize(question.System) ?? Normalize(question.PerformanceCategory) ?? Normalize(question.Discipline);
|
||||
|
||||
public static string? Normalize(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value)) return null;
|
||||
var trimmed = value.Trim();
|
||||
var exact = All.FirstOrDefault(system => string.Equals(system, trimmed, StringComparison.OrdinalIgnoreCase));
|
||||
if (exact is not null) return exact;
|
||||
var lower = trimmed.ToLowerInvariant();
|
||||
if (Has(lower, "human development", "well patient", "preventive", "health maintenance", "geriatr", "age-related", "neonat")) return All[0];
|
||||
if (Has(lower, "social science", "communication", "ethic", "patient safety", "professionalism", "systems-based")) return All[13];
|
||||
if (Has(lower, "pregnan", "obstetric", "gynec", "female reproductive", "breast", "puerper")) return All[10];
|
||||
if (Has(lower, "renal", "urinary", "male reproductive", "urolog", "nephro")) return All[9];
|
||||
if (Has(lower, "biostat", "epidemi", "population health", "medical literature")) return All[12];
|
||||
if (Has(lower, "immune", "immun", "blood", "hemat", "lymph", "multisystem", "infectious", "toxicolog", "poisoning", "oncolog", "trauma")) return All[1];
|
||||
if (Has(lower, "behavior", "psychi", "mental health")) return All[2];
|
||||
if (Has(lower, "nervous", "neuro", "special sense", "ophthalm", "eye & adnexa", "otolaryng", "ear, nose", " ent")) return All[3];
|
||||
if (Has(lower, "skin", "derm", "subcutaneous", "integument")) return All[4];
|
||||
if (Has(lower, "musculo", "orthop", "rheumat")) return All[5];
|
||||
if (Has(lower, "cardio", "circulatory", "peripheral vascular")) return All[6];
|
||||
if (Has(lower, "resp", "pulmon")) return All[7];
|
||||
if (Has(lower, "gastro", "digest", "hepato", "oral/dental")) return All[8];
|
||||
if (Has(lower, "endocr", "metabolic")) return All[11];
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool Has(string value, params string[] fragments) => fragments.Any(value.Contains);
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
# Question bank gamification
|
||||
|
||||
## Objective
|
||||
|
||||
Make studying easy to begin, satisfying to continue, and valuable to repeat while
|
||||
optimizing for durable learning rather than compulsive use. The core engagement
|
||||
loop is:
|
||||
|
||||
```text
|
||||
Answer -> immediate insight -> visible progress -> useful reward -> clear next goal
|
||||
```
|
||||
|
||||
Success means learners return consistently, improve weak areas, complete spaced
|
||||
reviews, and become more exam-ready. Raw time in the application is not itself a
|
||||
success metric.
|
||||
|
||||
## Product principles
|
||||
|
||||
- Reward demonstrated learning, recovery from errors, and breadth of coverage.
|
||||
- Make reward rules understandable and resistant to farming.
|
||||
- Give every study session a user-selected finish line and satisfying conclusion.
|
||||
- Keep stopping controls as prominent as continuation controls.
|
||||
- Do not lock core questions, explanations, or remediation behind RZ Coin.
|
||||
- Prefer weekly consistency over an indefinitely escalating daily streak.
|
||||
- Keep social participation optional and private by default.
|
||||
- Never use paid randomized rewards, near-miss effects, disguised odds, or
|
||||
loss-heavy messaging.
|
||||
|
||||
## Mastery paths
|
||||
|
||||
Show mastery for each system, topic, and tested concept. Mastery should combine:
|
||||
|
||||
- First-attempt correctness
|
||||
- Question difficulty
|
||||
- Recency and spaced-review status
|
||||
- Successful recovery after an incorrect answer
|
||||
- Breadth across related concepts
|
||||
|
||||
Mastery must not be a reskinned count of questions answered. Each topic should
|
||||
show its current state, evidence behind that state, and the next useful action.
|
||||
Suggested states are `Unseen`, `Learning`, `Practicing`, `Strong`, and `Mastered`.
|
||||
|
||||
The dashboard should highlight progress in weak areas and recently mastered
|
||||
concepts. A system cannot reach its highest rank by repeatedly farming a narrow
|
||||
set of easy questions.
|
||||
|
||||
## RZ Coin economy
|
||||
|
||||
Award RZ Coin for learning behaviors:
|
||||
|
||||
- A first correct attempt
|
||||
- Correctly answering a genuinely difficult question
|
||||
- Completing a scheduled review
|
||||
- Converting a previously missed concept into a correct delayed review
|
||||
- Improving a weak topic
|
||||
- Completing a timed or simulated-exam block
|
||||
- Meeting a balanced daily or weekly study goal
|
||||
|
||||
Do not primarily reward elapsed time, retries, repeated easy questions, or raw
|
||||
volume. Apply diminishing or zero rewards to repeated content where necessary.
|
||||
All awards should be server-authoritative, idempotent, and recorded in the coin
|
||||
ledger with a reason code.
|
||||
|
||||
Appropriate purchases include:
|
||||
|
||||
- Question-bank reset tokens
|
||||
- Cosmetic themes, avatars, profile frames, titles, and study environments
|
||||
- Optional analytics presentations
|
||||
- Limited streak protection
|
||||
- Collectible medical artifacts and achievement displays
|
||||
|
||||
Purchases must disclose their exact price and effect. Randomized paid purchases
|
||||
and purchasable power over exam scoring or question correctness are prohibited.
|
||||
|
||||
## Daily and weekly missions
|
||||
|
||||
Offer three short daily missions that reflect the learner's needs, for example:
|
||||
|
||||
- Complete 10 scheduled reviews.
|
||||
- Answer five difficulty-4 questions.
|
||||
- Improve one weak system.
|
||||
- Complete a timed block without skipping.
|
||||
|
||||
Allow a learner to replace a mission that does not fit the current study plan.
|
||||
Missions should not require excessive daily volume or encourage answering after
|
||||
fatigue has clearly degraded performance.
|
||||
|
||||
Weekly goals should emphasize consistency and breadth, such as studying on four
|
||||
days, reviewing three systems, or clearing a reasonable number of overdue
|
||||
reviews. A flexible weekly target is preferred to an endless daily streak.
|
||||
|
||||
## Session goals and completion
|
||||
|
||||
At the beginning of a session, let the learner choose a finish line:
|
||||
|
||||
- A number of questions
|
||||
- A study duration
|
||||
- One weak topic
|
||||
- One simulated-exam block
|
||||
- All currently due reviews, subject to a reasonable cap
|
||||
|
||||
At the finish line, present a completion screen before offering more work. The
|
||||
primary actions should give equal visual weight to `Finish for today` and a
|
||||
clearly sized optional next block. Do not create artificial unfinished states to
|
||||
pressure continuation.
|
||||
|
||||
The end-of-session summary should include:
|
||||
|
||||
- Questions completed and first-attempt accuracy
|
||||
- Concepts newly mastered
|
||||
- Previously missed concepts recovered
|
||||
- Weak areas improved
|
||||
- RZ Coin earned, with award reasons
|
||||
- Readiness change, with an explanation of how it was calculated
|
||||
- A recommended next session
|
||||
|
||||
## Progression and personal records
|
||||
|
||||
Introduce skill-based ranks such as `Intern`, `Resident`, `Chief`, and
|
||||
`Attending`. Rank advancement should require mastery across several systems,
|
||||
successful delayed reviews, and performance on appropriately difficult items.
|
||||
It must not depend only on account age or total answers.
|
||||
|
||||
Personal records should measure meaningful improvement:
|
||||
|
||||
- Best timed-block accuracy at a defined difficulty mix
|
||||
- Longest first-attempt correct streak at difficulty 4 or higher
|
||||
- Most concepts mastered in a week
|
||||
- Largest reduction in overdue reviews
|
||||
- Greatest improvement in a previously weak content area
|
||||
|
||||
Record definitions must remain stable and visible so the learner understands
|
||||
what was achieved.
|
||||
|
||||
## Social features
|
||||
|
||||
Potential opt-in features include private study groups, cooperative weekly
|
||||
goals, friend challenges, and anonymous percentile bands. Prefer cooperative
|
||||
goals over leaderboards based on raw volume.
|
||||
|
||||
Do not expose failure, study time, predicted score, or inactivity publicly by
|
||||
default. Users must be able to leave a group, hide a statistic, and disable
|
||||
social notifications.
|
||||
|
||||
## Notifications and streak safeguards
|
||||
|
||||
- Let users choose notification categories, quiet hours, and frequency.
|
||||
- Use factual reminders rather than guilt, urgency, or threatened loss.
|
||||
- Allow recovery days and planned breaks.
|
||||
- Cap streak-protection use and describe it transparently.
|
||||
- Never send repeated prompts after a learner chooses to finish a session.
|
||||
- Detect unusually long sessions and offer a neutral break reminder.
|
||||
|
||||
## Recommended delivery order
|
||||
|
||||
### Phase 1: foundation
|
||||
|
||||
1. Define the mastery model and expose topic-level progress.
|
||||
2. Add server-authoritative RZ Coin awards for approved learning events.
|
||||
3. Add user-selected session goals and an end-of-session summary.
|
||||
4. Add daily missions with one replacement per day.
|
||||
|
||||
### Phase 2: progression
|
||||
|
||||
1. Add weekly consistency goals.
|
||||
2. Add breadth-based ranks and personal records.
|
||||
3. Expand the RZ Coin store with cosmetic and utility items.
|
||||
4. Add limited streak protection and recovery days.
|
||||
|
||||
### Phase 3: social and optimization
|
||||
|
||||
1. Add private cooperative groups and friend challenges.
|
||||
2. Personalize missions using weak-area and spaced-review data.
|
||||
3. Experiment with presentation and pacing under the guardrails below.
|
||||
|
||||
## Measurement
|
||||
|
||||
Primary metrics should measure learning and healthy retention:
|
||||
|
||||
- Weekly active learners completing at least one meaningful study goal
|
||||
- Scheduled-review completion rate
|
||||
- Delayed retention after 7, 21, and 45 days
|
||||
- Recovery rate for previously missed concepts
|
||||
- Breadth of systems studied
|
||||
- Timed-block and simulation improvement
|
||||
- Percentage of sessions ending at the learner's selected finish line
|
||||
|
||||
Monitor guardrail metrics alongside product experiments:
|
||||
|
||||
- Extremely long sessions and late-night continuation
|
||||
- Rapid repetitive answering consistent with coin farming
|
||||
- Notification opt-out and mute rates
|
||||
- Mission abandonment after oversized goals
|
||||
- Increased accuracy volatility associated with fatigue
|
||||
- Support reports involving pressure, anxiety, or confusing rewards
|
||||
|
||||
Do not optimize an experiment solely for session length, questions per session,
|
||||
notification opens, or coin spending. A change should improve at least one
|
||||
learning or healthy-retention metric without materially degrading the guardrails.
|
||||
|
||||
## Initial acceptance criteria
|
||||
|
||||
The first release is complete when:
|
||||
|
||||
- Mastery is computed from correctness, difficulty, recency, and review outcomes.
|
||||
- Coin awards are idempotent and cannot be earned repeatedly from the same event.
|
||||
- Every award and purchase has a ledger entry and visible reason.
|
||||
- A learner can set and complete a bounded session goal.
|
||||
- The completion screen offers both stopping and continuing without deceptive
|
||||
emphasis.
|
||||
- Daily missions target due reviews or genuine coverage gaps and can be replaced.
|
||||
- Core educational content remains available without coin purchases.
|
||||
- Product analytics include the primary and guardrail metrics above.
|
||||
@@ -53,6 +53,11 @@ var rating_request_token := ""
|
||||
var report_request_token := ""
|
||||
var pending_excluded_question_ids: Array = []
|
||||
var pending_require_media := false
|
||||
var pending_step3_system := ""
|
||||
var pending_difficulty_min := 0
|
||||
var pending_difficulty_max := 0
|
||||
var pending_limit := 1
|
||||
var targeted_fallback_used := false
|
||||
var image_question_search_attempts := 0
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -107,7 +112,8 @@ func _ready() -> void:
|
||||
_process_attempt_queue()
|
||||
|
||||
|
||||
func load_questions_from_api(reset_retry: bool = true, excluded_question_ids: Array = [], require_media: bool = false) -> void:
|
||||
func load_questions_from_api(reset_retry: bool = true, excluded_question_ids: Array = [], require_media: bool = false,
|
||||
step3_system: String = "", difficulty_min: int = 0, difficulty_max: int = 0, requested_limit: int = 1) -> void:
|
||||
if not AuthState.can_send_authenticated_request():
|
||||
_set_load_error("Login required before loading questions.", false)
|
||||
return
|
||||
@@ -115,6 +121,11 @@ func load_questions_from_api(reset_retry: bool = true, excluded_question_ids: Ar
|
||||
if reset_retry:
|
||||
pending_excluded_question_ids = excluded_question_ids.duplicate()
|
||||
pending_require_media = require_media
|
||||
pending_step3_system = step3_system
|
||||
pending_difficulty_min = difficulty_min
|
||||
pending_difficulty_max = difficulty_max
|
||||
pending_limit = clampi(requested_limit, 1, 40)
|
||||
targeted_fallback_used = false
|
||||
image_question_search_attempts = 0
|
||||
retry_attempt = 0
|
||||
next_retry_seconds = 0
|
||||
@@ -144,11 +155,17 @@ func load_questions_from_api(reset_retry: bool = true, excluded_question_ids: Ar
|
||||
"Authorization: Bearer %s" % questions_request_token
|
||||
])
|
||||
var payload := {
|
||||
"limit": 1,
|
||||
"limit": pending_limit,
|
||||
"supportsChoicePools": true,
|
||||
"requireMedia": pending_require_media,
|
||||
"excludeExternalIds": pending_excluded_question_ids
|
||||
}
|
||||
if not pending_step3_system.is_empty():
|
||||
payload["step3System"] = pending_step3_system
|
||||
if pending_difficulty_min > 0:
|
||||
payload["difficultyMin"] = pending_difficulty_min
|
||||
if pending_difficulty_max > 0:
|
||||
payload["difficultyMax"] = pending_difficulty_max
|
||||
var err := request.request(questions_api_url, headers, HTTPClient.METHOD_POST, JSON.stringify(payload))
|
||||
if err != OK:
|
||||
_set_load_error("Unable to connect to the question service.")
|
||||
@@ -177,6 +194,12 @@ func _on_api_questions_loaded(result: int, response_code: int, _headers: PackedS
|
||||
api_questions.append(normalized)
|
||||
|
||||
if api_questions.is_empty():
|
||||
if not pending_step3_system.is_empty() and (pending_difficulty_min > 0 or pending_difficulty_max > 0) and not targeted_fallback_used:
|
||||
targeted_fallback_used = true
|
||||
pending_difficulty_min = 0
|
||||
pending_difficulty_max = 0
|
||||
call_deferred("load_questions_from_api", false, pending_excluded_question_ids, pending_require_media)
|
||||
return
|
||||
_set_load_error("Questions API returned no usable questions.", false)
|
||||
return
|
||||
if pending_require_media and not _contains_question_with_media(api_questions):
|
||||
@@ -200,6 +223,10 @@ func _on_api_questions_loaded(result: int, response_code: int, _headers: PackedS
|
||||
questions_changed.emit()
|
||||
|
||||
|
||||
func load_targeted_questions(step3_system: String, question_count: int, difficulty_min: int = 0, difficulty_max: int = 0) -> void:
|
||||
load_questions_from_api(true, [], false, step3_system, difficulty_min, difficulty_max, question_count)
|
||||
|
||||
|
||||
func retry_now() -> void:
|
||||
retry_attempt = 0
|
||||
next_retry_seconds = 0
|
||||
@@ -256,6 +283,11 @@ func clear_questions() -> void:
|
||||
pending_report_question_id = ""
|
||||
pending_excluded_question_ids.clear()
|
||||
pending_require_media = false
|
||||
pending_step3_system = ""
|
||||
pending_difficulty_min = 0
|
||||
pending_difficulty_max = 0
|
||||
pending_limit = 1
|
||||
targeted_fallback_used = false
|
||||
image_question_search_attempts = 0
|
||||
questions_changed.emit()
|
||||
ratings_changed.emit()
|
||||
@@ -337,7 +369,7 @@ func get_questions_for_system(system_name: String) -> Array:
|
||||
|
||||
var filtered: Array = []
|
||||
for question in questions:
|
||||
if str(question.get("system", "")) == system_name:
|
||||
if str(question.get("step3_system", question.get("system", ""))) == system_name or str(question.get("system", "")) == system_name:
|
||||
filtered.append(question.duplicate(true))
|
||||
return filtered
|
||||
|
||||
@@ -688,6 +720,7 @@ func _normalize_api_question(item: Variant) -> Dictionary:
|
||||
"choice_selection_percentages": item.get("choiceSelectionPercentages", item.get("choice_selection_percentages", {})),
|
||||
"discipline": str(item.get("discipline", "")),
|
||||
"system": str(item.get("system", "")),
|
||||
"step3_system": str(item.get("step3System", item.get("step3_system", ""))),
|
||||
"performance_category": str(item.get("performanceCategory", item.get("performance_category", ""))),
|
||||
"difficulty": int(item.get("difficulty", 1)),
|
||||
"question_elo": int(item.get("questionElo", item.get("question_elo", 1000))),
|
||||
|
||||
@@ -24,6 +24,7 @@ var cleared_count := 0
|
||||
var retained_count := 0
|
||||
var outbox: Array = []
|
||||
var active_account_key := ""
|
||||
var step3_system := ""
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -48,7 +49,8 @@ func _ready() -> void:
|
||||
_on_auth_changed()
|
||||
|
||||
|
||||
func start_pass() -> void:
|
||||
func start_pass(requested_step3_system: String = "") -> void:
|
||||
step3_system = requested_step3_system
|
||||
excluded_question_ids.clear()
|
||||
reviewed_count = 0
|
||||
cleared_count = 0
|
||||
@@ -68,6 +70,8 @@ func load_next() -> void:
|
||||
"excludeExternalIds": excluded_question_ids,
|
||||
"supportsChoicePools": true
|
||||
}
|
||||
if not step3_system.is_empty():
|
||||
payload["step3System"] = step3_system
|
||||
var error := next_request.request(
|
||||
"%s/next" % api_url,
|
||||
_headers(),
|
||||
|
||||
@@ -9,6 +9,8 @@ var size_spin: SpinBox
|
||||
var mode_option: OptionButton
|
||||
var status_label: Label
|
||||
var start_button: Button
|
||||
var requested_count := 10
|
||||
var targeted_setup := false
|
||||
|
||||
func _ready() -> void:
|
||||
set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
@@ -67,7 +69,16 @@ func _ready() -> void:
|
||||
|
||||
|
||||
func on_show(payload: Variant = null) -> void:
|
||||
selected_system = str(payload) if payload != null else "All Systems"
|
||||
if typeof(payload) == TYPE_DICTIONARY:
|
||||
targeted_setup = true
|
||||
selected_system = str(payload.get("step3_system", "All Systems"))
|
||||
requested_count = int(payload.get("question_count", 10))
|
||||
size_spin.value = requested_count
|
||||
QuestionBankService.load_targeted_questions(selected_system, requested_count,
|
||||
int(payload.get("difficulty_min", 0)), int(payload.get("difficulty_max", 0)))
|
||||
else:
|
||||
targeted_setup = false
|
||||
selected_system = str(payload) if payload != null else "All Systems"
|
||||
_refresh_setup()
|
||||
|
||||
|
||||
@@ -77,14 +88,17 @@ func _refresh_setup() -> void:
|
||||
system_label.text = "Zone: %s" % selected_system
|
||||
var available := QuestionBankService.get_questions_for_system(selected_system).size()
|
||||
size_spin.max_value = max(1, available)
|
||||
size_spin.value = min(size_spin.value, available)
|
||||
if not QuestionBankService.is_loading and available > 0:
|
||||
size_spin.value = min(requested_count, available) if targeted_setup else min(size_spin.value, available)
|
||||
targeted_setup = false
|
||||
start_button.disabled = available == 0 or QuestionBankService.is_loading
|
||||
if QuestionBankService.is_loading:
|
||||
status_label.text = "Question API unavailable. Retrying in %d seconds..." % QuestionBankService.next_retry_seconds if QuestionBankService.load_state == "retrying" else "Loading question bank..."
|
||||
elif available == 0:
|
||||
status_label.text = QuestionBankService.last_error if QuestionBankService.last_error != "" else "No questions are available for this zone yet."
|
||||
else:
|
||||
status_label.text = "%d questions available." % available
|
||||
status_label.text = "%d questions available.%s" % [available,
|
||||
" The requested difficulty range was broadened." if QuestionBankService.targeted_fallback_used else ""]
|
||||
|
||||
|
||||
func _start_block() -> void:
|
||||
|
||||
@@ -33,6 +33,7 @@ var accuracy_progress_bar: ProgressBar
|
||||
var pending_review_launch := false
|
||||
var review_queue_service: Variant
|
||||
var trend_mode := "rating"
|
||||
var mastery_rows: Array[BoxContainer] = []
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -63,6 +64,7 @@ func _build() -> void:
|
||||
metric_values.clear()
|
||||
metric_details.clear()
|
||||
metric_trends.clear()
|
||||
mastery_rows.clear()
|
||||
scroll = ScrollContainer.new()
|
||||
scroll.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||
@@ -131,7 +133,7 @@ func _build() -> void:
|
||||
performance.add_child(trend_chart)
|
||||
performance.add_child(HSeparator.new())
|
||||
var system_header := HBoxContainer.new()
|
||||
system_header.add_child(_section_title("Performance by system"))
|
||||
system_header.add_child(_section_title("System mastery path"))
|
||||
var header_space := Control.new()
|
||||
header_space.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
system_header.add_child(header_space)
|
||||
@@ -276,7 +278,7 @@ func _render_accuracy_card(data: Dictionary) -> void:
|
||||
accuracy_progress_bar.value = accuracy
|
||||
|
||||
|
||||
func _render_systems(systems_value: Array) -> void:
|
||||
func _render_systems_legacy(systems_value: Array) -> void:
|
||||
_clear(system_list)
|
||||
var systems := systems_value.duplicate(true)
|
||||
if systems.is_empty():
|
||||
@@ -331,7 +333,7 @@ func _render_systems(systems_value: Array) -> void:
|
||||
system_list.add_child(row)
|
||||
|
||||
|
||||
func _render_focus(systems: Array) -> void:
|
||||
func _render_focus_legacy(systems: Array) -> void:
|
||||
_clear(focus_content)
|
||||
focus_content.add_child(_section_title("Study focus"))
|
||||
if systems.is_empty():
|
||||
@@ -355,6 +357,143 @@ func _render_focus(systems: Array) -> void:
|
||||
focus_content.add_child(practice)
|
||||
|
||||
|
||||
func _render_systems(systems: Array) -> void:
|
||||
_clear(system_list)
|
||||
mastery_rows.clear()
|
||||
if systems.is_empty():
|
||||
system_list.add_child(_label("Answer questions to begin your mastery path.", 14, Brand.muted_color(UIThemeService.is_dark_mode())))
|
||||
return
|
||||
for item in systems:
|
||||
var container := VBoxContainer.new()
|
||||
container.add_theme_constant_override("separation", 6)
|
||||
var row := BoxContainer.new()
|
||||
row.vertical = false
|
||||
mastery_rows.append(row)
|
||||
row.add_theme_constant_override("separation", 10)
|
||||
var name_label := _label(str(item.get("system", "General")), 13, Brand.text_color(UIThemeService.is_dark_mode()), true)
|
||||
name_label.custom_minimum_size.x = 190
|
||||
name_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
name_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
row.add_child(name_label)
|
||||
var state := str(item.get("masteryState", "unseen"))
|
||||
var state_label := _label(state.capitalize(), 11, _mastery_color(state), true)
|
||||
state_label.custom_minimum_size.x = 74
|
||||
row.add_child(state_label)
|
||||
var bar := ProgressBar.new()
|
||||
bar.show_percentage = false
|
||||
bar.value = int(item.get("masteryScore", 0))
|
||||
bar.custom_minimum_size = Vector2(80, 7)
|
||||
bar.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
bar.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||
_style_progress(bar, _mastery_color(state))
|
||||
row.add_child(bar)
|
||||
var percentage := _label("%d%%" % int(item.get("masteryScore", 0)) if int(item.get("distinctQuestions", 0)) > 0 else "--", 14, Brand.ROYAL_BLUE, true)
|
||||
percentage.custom_minimum_size.x = 48
|
||||
row.add_child(percentage)
|
||||
var action: Dictionary = item.get("recommendedAction", {})
|
||||
var action_button := _button(str(action.get("label", "Practice")))
|
||||
action_button.custom_minimum_size.x = 155
|
||||
action_button.pressed.connect(_launch_mastery_action.bind(item))
|
||||
row.add_child(action_button)
|
||||
container.add_child(row)
|
||||
var evidence := "%d distinct | %d difficulty 4+ successful" % [int(item.get("distinctQuestions", 0)), int(item.get("difficulty4PlusSuccessful", 0))]
|
||||
if int(item.get("pendingReviews", 0)) > 0:
|
||||
evidence += " | %d review(s) pending" % int(item.get("pendingReviews", 0))
|
||||
elif bool(item.get("refreshDue", false)):
|
||||
evidence += " | Refresh due"
|
||||
container.add_child(_label(evidence, 11, Brand.muted_color(UIThemeService.is_dark_mode())))
|
||||
var requirements := _label(_mastery_requirement(item), 11, Brand.muted_color(UIThemeService.is_dark_mode()))
|
||||
requirements.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
requirements.visible = false
|
||||
var requirement_toggle := Button.new()
|
||||
requirement_toggle.text = "Show requirements"
|
||||
requirement_toggle.flat = true
|
||||
requirement_toggle.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||
requirement_toggle.pressed.connect(func() -> void:
|
||||
requirements.visible = not requirements.visible
|
||||
requirement_toggle.text = "Hide requirements" if requirements.visible else "Show requirements")
|
||||
container.add_child(requirement_toggle)
|
||||
container.add_child(requirements)
|
||||
system_list.add_child(container)
|
||||
|
||||
|
||||
func _render_focus(systems: Array) -> void:
|
||||
_clear(focus_content)
|
||||
focus_content.add_child(_section_title("Study focus"))
|
||||
if systems.is_empty():
|
||||
focus_content.add_child(_label("Complete a few questions to receive a focused recommendation.", 14, Brand.muted_color(UIThemeService.is_dark_mode())))
|
||||
return
|
||||
var weakest: Dictionary = systems[0]
|
||||
for item in systems:
|
||||
if _is_higher_focus(item, weakest):
|
||||
weakest = item
|
||||
focus_content.add_child(_label("Recommended focus", 12, Brand.muted_color(UIThemeService.is_dark_mode())))
|
||||
focus_content.add_child(_label(str(weakest.get("system", "General")), 23, Brand.PURPLE, true))
|
||||
focus_content.add_child(_label("%s | %d%% mastery across %d questions" % [str(weakest.get("masteryState", "unseen")).capitalize(), int(weakest.get("masteryScore", 0)), int(weakest.get("distinctQuestions", 0))], 13, Brand.text_color(UIThemeService.is_dark_mode())))
|
||||
var progress := ProgressBar.new()
|
||||
progress.show_percentage = false
|
||||
progress.value = int(weakest.get("masteryScore", 0))
|
||||
progress.custom_minimum_size.y = 8
|
||||
_style_progress(progress, Brand.PURPLE)
|
||||
focus_content.add_child(progress)
|
||||
var action: Dictionary = weakest.get("recommendedAction", {})
|
||||
var practice := _button(str(action.get("label", "Start learning")))
|
||||
practice.pressed.connect(_launch_mastery_action.bind(weakest))
|
||||
focus_content.add_child(practice)
|
||||
|
||||
|
||||
func _mastery_color(state: String) -> Color:
|
||||
match state:
|
||||
"mastered": return Brand.SUCCESS
|
||||
"strong": return Brand.TEAL
|
||||
"practicing": return Brand.ROYAL_BLUE
|
||||
"learning": return Brand.AMBER
|
||||
_: return Brand.muted_color(UIThemeService.is_dark_mode())
|
||||
|
||||
|
||||
func _mastery_requirement(item: Dictionary) -> String:
|
||||
match str(item.get("masteryState", "unseen")):
|
||||
"unseen": return "Answer your first question in this system to begin the path."
|
||||
"learning": return "Reach 5 distinct questions to enter Practicing."
|
||||
"practicing": return "Strong requires 15 distinct questions and at least 75% mastery."
|
||||
"strong": return "Mastered requires 25 distinct questions, 85% mastery, 5 successful difficulty 4+ questions, and no unresolved reviews."
|
||||
_:
|
||||
return "Complete a difficulty 4-5 refresh when due to keep this mastery current." if bool(item.get("refreshDue", false)) else "This system currently meets every mastery requirement."
|
||||
|
||||
|
||||
func _mastery_priority(item: Dictionary) -> int:
|
||||
if int(item.get("pendingReviews", 0)) > 0:
|
||||
return -1000 + int(item.get("masteryScore", 0))
|
||||
var ranks := {"unseen": 0, "learning": 1, "practicing": 2, "strong": 3, "mastered": 4}
|
||||
return int(ranks.get(str(item.get("masteryState", "unseen")), 0)) * 100 + int(item.get("masteryScore", 0))
|
||||
|
||||
|
||||
func _is_higher_focus(candidate: Dictionary, current: Dictionary) -> bool:
|
||||
var candidate_priority := _mastery_priority(candidate)
|
||||
var current_priority := _mastery_priority(current)
|
||||
if candidate_priority != current_priority:
|
||||
return candidate_priority < current_priority
|
||||
var candidate_date := str(candidate.get("lastEvidenceUtc", ""))
|
||||
var current_date := str(current.get("lastEvidenceUtc", ""))
|
||||
if candidate_date.is_empty():
|
||||
return not current_date.is_empty()
|
||||
return not current_date.is_empty() and candidate_date < current_date
|
||||
|
||||
|
||||
func _launch_mastery_action(item: Dictionary) -> void:
|
||||
var action: Dictionary = item.get("recommendedAction", {})
|
||||
var system_name := str(item.get("system", ""))
|
||||
if str(action.get("type", "practice")) == "review":
|
||||
_start_review_queue(system_name)
|
||||
return
|
||||
main.show_screen("setup", {
|
||||
"step3_system": system_name,
|
||||
"question_count": int(action.get("questionCount", 10)),
|
||||
"difficulty_min": int(action.get("difficultyMin", 0)) if action.get("difficultyMin") != null else 0,
|
||||
"difficulty_max": int(action.get("difficultyMax", 0)) if action.get("difficultyMax") != null else 0
|
||||
})
|
||||
|
||||
|
||||
func _render_review(data: Dictionary) -> void:
|
||||
_clear(review_list)
|
||||
review_list.add_child(_section_title("Review queue"))
|
||||
@@ -495,12 +634,12 @@ func _review_action(text: String, icon_name: String, accent: Color, route: Strin
|
||||
return button
|
||||
|
||||
|
||||
func _start_review_queue() -> void:
|
||||
func _start_review_queue(step3_system: String = "") -> void:
|
||||
if pending_review_launch:
|
||||
return
|
||||
pending_review_launch = true
|
||||
_show_loading_message("Preparing your review queue...")
|
||||
review_queue_service.start_pass()
|
||||
review_queue_service.start_pass(step3_system)
|
||||
_render_review(DashboardService.data)
|
||||
|
||||
|
||||
@@ -673,6 +812,12 @@ func _update_responsive() -> void:
|
||||
lower_grid.columns = 1 if compact else 2
|
||||
if trend_chart != null:
|
||||
trend_chart.custom_minimum_size.x = 300.0 if compact else 520.0
|
||||
var narrow := size.x < 700.0
|
||||
for row in mastery_rows:
|
||||
row.vertical = narrow
|
||||
for child in row.get_children():
|
||||
if child is Button:
|
||||
child.custom_minimum_size.x = 0.0 if narrow else 155.0
|
||||
|
||||
|
||||
func _clear(container: Container) -> void:
|
||||
|
||||
@@ -19,6 +19,12 @@ func _run() -> void:
|
||||
await process_frame
|
||||
|
||||
_assert(dashboard.system_list.get_child_count() == 14, "dashboard should render all 14 Step 3 systems")
|
||||
_assert(dashboard.mastery_rows.size() == 14, "dashboard should render one mastery action row per system")
|
||||
var first_mastery_row: BoxContainer = dashboard.mastery_rows[0]
|
||||
_assert(not first_mastery_row.vertical, "desktop mastery rows should remain horizontal")
|
||||
dashboard.size = Vector2(640, 900)
|
||||
dashboard._update_responsive()
|
||||
_assert(first_mastery_row.vertical, "narrow mastery rows should stack to avoid clipping")
|
||||
_assert(dashboard.rating_toggle.button_pressed, "rating should be the default trend mode")
|
||||
_assert(dashboard.trend_chart.mode == "rating", "chart should initially render rating history")
|
||||
dashboard._set_trend_mode("accuracy")
|
||||
@@ -38,7 +44,14 @@ func _dashboard_data() -> Dictionary:
|
||||
"system": "System %02d" % index,
|
||||
"correct": 1 if index == 0 else 0,
|
||||
"total": 1 if index == 0 else 0,
|
||||
"accuracy": 100 if index == 0 else 0
|
||||
"accuracy": 100 if index == 0 else 0,
|
||||
"masteryScore": 82 if index == 0 else 0,
|
||||
"masteryState": "strong" if index == 0 else "unseen",
|
||||
"distinctQuestions": 15 if index == 0 else 0,
|
||||
"difficulty4PlusSuccessful": 4 if index == 0 else 0,
|
||||
"pendingReviews": 0,
|
||||
"refreshDue": false,
|
||||
"recommendedAction": {"type": "practice", "label": "Challenge this system", "questionCount": 10, "difficultyMin": 4, "difficultyMax": 5}
|
||||
})
|
||||
var trend: Array = []
|
||||
for index in range(30):
|
||||
|
||||
@@ -251,10 +251,12 @@ func _test_review_content_normalization() -> void:
|
||||
"teachingPoint": "Teaching"
|
||||
}],
|
||||
"system": "System",
|
||||
"step3System": "Cardiovascular System",
|
||||
"discipline": "Medicine"
|
||||
})
|
||||
_assert(normalized.get("key_takeaway", "") == "Takeaway", "key takeaway should be normalized")
|
||||
_assert(normalized.get("usmle_pearl", "") == "Pearl", "USMLE pearl should be normalized")
|
||||
_assert(normalized.get("step3_system", "") == "Cardiovascular System", "official Step 3 system should be normalized")
|
||||
var review_followups: Array = normalized.get("review_followups", [])
|
||||
_assert(review_followups.size() == 1, "review follow-ups should be normalized")
|
||||
_assert(review_followups[0].get("correct_choice_id", "") == "B", "review follow-up answer keys should be normalized")
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup><OutputType>Exe</OutputType><TargetFramework>net8.0</TargetFramework><ImplicitUsings>enable</ImplicitUsings><Nullable>enable</Nullable></PropertyGroup>
|
||||
<ItemGroup><ProjectReference Include="../../api/QuestionsApi/QuestionsApi.csproj" /></ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,105 @@
|
||||
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)
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,358 @@
|
||||
internal static class SeedCatalog
|
||||
{
|
||||
private static Seed Q(string slug, string system, string discipline, string topic, string stem, string leadIn, string[] answers, string rationale, string takeaway, string key, string context, string neutral, string reference, string task = "Behavioral Health & Social Sciences: Communication, Ethics, Patient Safety")
|
||||
=> new(slug, system, discipline, task, topic, stem, leadIn, answers, rationale, takeaway, key, context, neutral, reference);
|
||||
|
||||
internal static readonly Seed[] All =
|
||||
[
|
||||
Q("postpartum-psychosis-emergency", "Behavioral Health", "Psychiatry", "postpartum psychosis",
|
||||
"A 29-year-old woman 8 days after delivery has slept 2 hours in 4 days. She says her infant is possessed and reports a command to drown him. Her partner has moved the infant to another room. She is euphoric, disorganized, and refuses outpatient care.",
|
||||
"Which is the most appropriate next step?",
|
||||
["Emergency psychiatric hospitalization with protection of the infant", "Routine follow-up in 6 weeks", "Reassurance that postpartum blues will resolve", "Psychotherapy alone at home", "Allow unsupervised infant care to preserve bonding"],
|
||||
"Delusions, mania, severe insomnia, and commands to harm an infant indicate postpartum psychosis with immediate danger. Inpatient treatment and separation from the infant until safe are required.",
|
||||
"Postpartum psychosis is a psychiatric emergency requiring hospitalization and immediate protection of parent and infant.", "reports a command to drown him", "8 days after delivery", "her partner has moved the infant to another room", "APA"),
|
||||
|
||||
Q("catatonia-lorazepam-challenge", "Behavioral Health", "Psychiatry", "catatonia",
|
||||
"A 43-year-old man with bipolar disorder becomes mute and immobile. He maintains imposed postures, resists repositioning, and repeats the examiner's words. Temperature and autonomic findings are normal. Head CT and metabolic testing are unrevealing.",
|
||||
"Which intervention is most appropriate now?",
|
||||
["Administer a lorazepam challenge", "Begin high-potency antipsychotic monotherapy", "Use physical restraints as definitive treatment", "Discharge for outpatient psychotherapy", "Wait for fever before treating"],
|
||||
"Mutism, posturing, negativism, and echolalia support catatonia. A benzodiazepine challenge is both diagnostically informative and first-line treatment; electroconvulsive therapy is used when severe or refractory.",
|
||||
"Recognize catatonia promptly and treat first with benzodiazepines while addressing the cause.", "maintains imposed postures", "temperature and autonomic findings are normal", "head CT and metabolic testing are unrevealing", "APA"),
|
||||
|
||||
Q("delirium-hypoactive-medication-review", "Behavioral Health", "Geriatrics", "hypoactive delirium",
|
||||
"An 82-year-old hospitalized woman becomes withdrawn and inattentive after surgery. Her alertness fluctuates during the interview. Diphenhydramine and oxybutynin were added yesterday. She has no focal deficit and was cognitively independent before admission.",
|
||||
"Which is the best initial management?",
|
||||
["Stop deliriogenic medications and institute multicomponent supportive care", "Diagnose major depression and discharge", "Start a benzodiazepine nightly", "Apply restraints continuously", "Begin a cholinesterase inhibitor for dementia"],
|
||||
"Acute fluctuating inattention is hypoactive delirium. Removing anticholinergic contributors and using orientation, mobilization, sleep, hydration, hearing, and vision support treats causes and reduces harm.",
|
||||
"Hypoactive delirium is often missed; identify precipitants and use nonpharmacologic supportive measures.", "alertness fluctuates", "diphenhydramine and oxybutynin were added yesterday", "no focal deficit", "APA"),
|
||||
|
||||
Q("alcohol-withdrawal-delirium-benzodiazepines", "Behavioral Health", "Medicine", "alcohol withdrawal",
|
||||
"A 51-year-old man hospitalized for pancreatitis becomes tremulous and agitated 72 hours after his last drink. He sees insects on the wall, pulse is 126/min, blood pressure is 178/102 mm Hg, and temperature is 38.1 C. Glucose and oxygenation are normal.",
|
||||
"Which treatment should be prioritized?",
|
||||
["Symptom-triggered intravenous benzodiazepines with close monitoring", "Haloperidol alone", "Propranolol alone", "Disulfiram immediately", "Physical restraints without medication"],
|
||||
"The timing, autonomic hyperactivity, agitation, and visual hallucinations indicate severe alcohol withdrawal. Benzodiazepines prevent seizures and progression; adjuncts do not replace them.",
|
||||
"Treat severe alcohol withdrawal with adequately dosed benzodiazepines and supportive monitoring.", "72 hours after his last drink", "sees insects on the wall", "glucose and oxygenation are normal", "SAMHSA"),
|
||||
|
||||
Q("serotonin-syndrome-linezolid", "Behavioral Health", "Medicine", "serotonin syndrome",
|
||||
"A 46-year-old woman taking fluoxetine receives linezolid. Thirty-six hours later she has agitation, diaphoresis, diarrhea, inducible ankle clonus, ocular clonus, and hyperreflexia greater in the legs. Temperature is 40.2 C and muscle tone is increased.",
|
||||
"Which immediate management is most appropriate?",
|
||||
["Stop serotonergic agents, provide supportive care and benzodiazepines, and consider cyproheptadine", "Restart fluoxetine to avoid withdrawal", "Continue linezolid and give dantrolene", "Perform hemodialysis to remove both drugs", "Give bromocriptine and observe"],
|
||||
"Clonus and hyperreflexia after serotonergic drug interaction establish serotonin syndrome. Remove serotonergic drugs, cool and sedate supportively, and use cyproheptadine for persistent moderate-to-severe toxicity.",
|
||||
"Clonus distinguishes serotonin toxicity; stop causative agents and rapidly control hyperthermia and agitation.", "inducible ankle clonus", "receives linezolid", "temperature is 40.2 C", "APA"),
|
||||
|
||||
Q("anorexia-bradycardia-inpatient", "Behavioral Health", "Psychiatry", "eating disorder instability",
|
||||
"A 19-year-old woman with restrictive eating has a body mass index of 14.2 kg/m2. Supine pulse is 42/min, blood pressure is 84/50 mm Hg, potassium is 3.0 mEq/L, and she becomes presyncopal on standing. She requests another outpatient nutrition visit.",
|
||||
"Which disposition is most appropriate?",
|
||||
["Medical hospitalization with monitored nutritional rehabilitation", "Weekly outpatient weighing only", "Begin a stimulant for fatigue", "Prescribe exercise to restore bone mass", "Delay care until amenorrhea lasts one year"],
|
||||
"Severe malnutrition with bradycardia, hypotension, orthostasis, and hypokalemia requires medical stabilization and monitored refeeding because arrhythmia and refeeding complications are imminent.",
|
||||
"Physiologic instability in an eating disorder mandates medical hospitalization regardless of patient reassurance.", "pulse is 42/min", "body mass index of 14.2 kg/m2", "she requests another outpatient nutrition visit", "APA"),
|
||||
|
||||
Q("capacity-treatment-refusal", "Professionalism and Ethics", "Ethics", "decision-making capacity",
|
||||
"A man with schizophrenia and a gangrenous foot refuses amputation. He accurately explains that without surgery he may die, describes the operation and alternatives, consistently values avoiding dependence over longevity, and is not responding to internal stimuli. His family demands surgery.",
|
||||
"Which is the most appropriate action?",
|
||||
["Honor the refusal after documenting decision-making capacity", "Obtain family consent and operate", "Assume schizophrenia eliminates capacity", "Seek an emergency court order solely because death is possible", "Sedate him and perform surgery"],
|
||||
"Capacity is decision-specific and requires understanding, appreciation, reasoning, and communication of a stable choice. Psychiatric diagnosis alone does not negate it, and a capacitated adult may refuse life-saving care.",
|
||||
"Respect a capacitated patient's informed refusal even when clinicians or family strongly disagree.", "accurately explains that without surgery he may die", "has schizophrenia", "his family demands surgery", "AMA"),
|
||||
|
||||
Q("emergency-unrepresented-consent", "Professionalism and Ethics", "Ethics", "emergency consent",
|
||||
"An unconscious unidentified patient has a ruptured abdominal aortic aneurysm and will die without immediate surgery. No advance directive or surrogate can be located despite rapid efforts. Delaying for a court order would make repair futile.",
|
||||
"What should the physician do?",
|
||||
["Proceed with emergency surgery under presumed consent", "Wait for a court-appointed guardian", "Provide comfort care because written consent is absent", "Ask hospital security to consent", "Postpone until identity is confirmed"],
|
||||
"Emergency treatment may proceed when delay threatens life, the patient lacks capacity, and no surrogate or directive is available. Care should reflect the patient's presumed interests.",
|
||||
"Use presumed consent for immediately necessary emergency care when no authorized decision-maker is available.", "will die without immediate surgery", "no advance directive or surrogate", "patient is unidentified", "AMA"),
|
||||
|
||||
Q("minor-confidential-sti-care", "Professionalism and Ethics", "Pediatrics", "adolescent confidentiality",
|
||||
"A 16-year-old requests testing for gonorrhea after unprotected intercourse and asks that her parent not be told. She denies coercion, abuse, self-harm, and danger at home. She understands testing and treatment. Local law permits minors to consent to STI care.",
|
||||
"Which response is most appropriate?",
|
||||
["Provide confidential STI evaluation while encouraging supportive adult involvement", "Refuse testing without parental permission", "Call the parent before obtaining specimens", "Notify the school", "Report consensual activity as abuse solely because she is a minor"],
|
||||
"Where law permits, adolescents may consent confidentially to STI services. Confidentiality improves access; clinicians should explain its limits and encourage, but not require, safe adult support.",
|
||||
"Protect legally authorized adolescent confidentiality unless a safety or mandatory-reporting exception applies.", "local law permits minors to consent to STI care", "denies coercion", "asks that her parent not be told", "CDC-STI"),
|
||||
|
||||
Q("impaired-colleague-reporting", "Professionalism and Ethics", "Ethics", "impaired physician",
|
||||
"A resident smells alcohol on a surgeon before an emergency case. The surgeon has slurred speech and nearly falls while scrubbing. The department chair is not answering, and the surgeon orders the resident to remain silent.",
|
||||
"What is the resident's immediate obligation?",
|
||||
["Prevent the surgeon from operating and activate the institutional safety chain", "Obey because the surgeon is senior", "Wait to see whether an error occurs", "Post anonymously on social media", "Confront the surgeon after the case only"],
|
||||
"Observable impairment creates an immediate patient threat. The resident must remove the clinician from care through available supervisory or safety channels and ensure coverage, with subsequent formal reporting.",
|
||||
"Patient safety overrides hierarchy when a clinician appears impaired during care.", "slurred speech", "before an emergency case", "department chair is not answering", "AMA"),
|
||||
|
||||
Q("error-disclosure", "Professionalism and Ethics", "Ethics", "medical error disclosure",
|
||||
"A physician accidentally orders ten times the intended insulin dose. The error causes symptomatic hypoglycemia that is promptly reversed without lasting injury. The patient asks why emergency dextrose was required. Risk management suggests saying only that glucose fell.",
|
||||
"Which response is most appropriate?",
|
||||
["Disclose the dosing error, its effects, treatment, and prevention plan", "Withhold the error because no permanent harm occurred", "Blame the nurse who administered the order", "Alter the record to show the intended dose", "Wait until the patient obtains the chart"],
|
||||
"Patients should receive timely, factual disclosure of harmful errors, including consequences, corrective care, and prevention steps. Lack of permanent injury does not remove this duty.",
|
||||
"Disclose clinically significant errors honestly and explain mitigation and prevention.", "ten times the intended insulin dose", "without lasting injury", "risk management suggests saying only that glucose fell", "AMA"),
|
||||
|
||||
Q("confidentiality-driving-risk", "Professionalism and Ethics", "Neurology", "confidentiality exception",
|
||||
"A commercial bus driver with uncontrolled focal impaired-awareness seizures continues driving and refuses to notify the licensing authority. He had two episodes while driving this month. The physician has reviewed applicable state law, which mandates clinician reporting of unsafe drivers.",
|
||||
"Which action is appropriate?",
|
||||
["Report the minimum necessary information to the designated authority", "Maintain absolute confidentiality", "Notify the driver's employer and all passengers", "Post a warning publicly", "Prescribe a placebo and take no other action"],
|
||||
"Confidentiality has narrow exceptions when law mandates reporting to prevent serious harm. Disclosure should go only to the proper authority and be limited to necessary information.",
|
||||
"When reporting is legally required to prevent serious harm, disclose only what is necessary through the proper channel.", "state law mandates clinician reporting", "two episodes while driving this month", "commercial bus driver", "AMA"),
|
||||
|
||||
Q("lung-cancer-screening-ldct", "Preventive Medicine & Health Maintenance", "Medicine", "lung cancer screening",
|
||||
"A 61-year-old man has a 28-pack-year smoking history and quit 6 years ago. He has no cough, weight loss, or hemoptysis and is healthy enough for curative lung surgery. Chest examination is normal.",
|
||||
"Which screening strategy is appropriate?",
|
||||
["Annual low-dose chest CT", "Annual chest radiography", "Sputum cytology every 6 months", "No screening until symptoms develop", "One-time contrast chest CT only"],
|
||||
"An asymptomatic adult age 50-80 with at least 20 pack-years who currently smokes or quit within 15 years should be offered annual low-dose CT after shared decision-making.",
|
||||
"Use annual low-dose CT, not chest radiography, for eligible high-risk adults.", "28-pack-year smoking history", "quit 6 years ago", "no cough", "USPSTF", "Preventive Medicine & Health Maintenance"),
|
||||
|
||||
Q("aaa-screening", "Preventive Medicine & Health Maintenance", "Medicine", "abdominal aortic aneurysm screening",
|
||||
"A healthy 68-year-old man smoked one pack daily for 25 years before quitting. He has never undergone abdominal imaging. Blood pressure is controlled and abdominal examination is normal.",
|
||||
"Which preventive service should be offered?",
|
||||
["One-time abdominal ultrasonography", "Annual abdominal CT", "No screening because he quit smoking", "Carotid ultrasonography", "Serial abdominal radiographs"],
|
||||
"Men age 65-75 who have ever smoked benefit from one-time ultrasound screening for abdominal aortic aneurysm.",
|
||||
"Offer one-time abdominal ultrasound to men age 65-75 who have ever smoked.", "68-year-old man", "smoked one pack daily for 25 years", "abdominal examination is normal", "USPSTF", "Preventive Medicine & Health Maintenance"),
|
||||
|
||||
Q("osteoporosis-screening-dxa", "Preventive Medicine & Health Maintenance", "Medicine", "osteoporosis screening",
|
||||
"A 66-year-old woman presents for preventive care. She has never fractured a bone, takes no glucocorticoids, and walks daily. Menopause occurred at age 51 and physical examination is normal.",
|
||||
"Which screening is recommended now?",
|
||||
["Dual-energy x-ray absorptiometry", "Plain radiographs of the spine", "Serum calcium alone", "No screening without fracture", "Bone biopsy"],
|
||||
"Routine osteoporosis screening with DXA is recommended for women age 65 years or older, independent of additional risk factors.",
|
||||
"Screen women age 65 or older for osteoporosis with DXA.", "66-year-old woman", "has never fractured a bone", "walks daily", "USPSTF", "Preventive Medicine & Health Maintenance"),
|
||||
|
||||
Q("colorectal-screening-positive-fit", "Preventive Medicine & Health Maintenance", "Medicine", "colorectal cancer screening",
|
||||
"A 58-year-old asymptomatic woman completes an annual fecal immunochemical test, which is positive. Hemoglobin is normal, she has no family history of colorectal cancer, and her last colonoscopy was 12 years ago.",
|
||||
"Which is the most appropriate next step?",
|
||||
["Diagnostic colonoscopy", "Repeat the fecal test immediately", "CT abdomen without bowel preparation", "Reassure because hemoglobin is normal", "Begin aspirin and repeat testing next year"],
|
||||
"A positive stool-based colorectal screening test requires colonoscopy to identify and remove the bleeding lesion; repeating the stool test does not complete screening.",
|
||||
"Follow every positive stool-based colorectal cancer screen with timely colonoscopy.", "fecal immunochemical test, which is positive", "hemoglobin is normal", "last colonoscopy was 12 years ago", "USPSTF", "Preventive Medicine & Health Maintenance"),
|
||||
|
||||
Q("cervical-screening-immunized", "Preventive Medicine & Health Maintenance", "Gynecology", "cervical cancer screening",
|
||||
"A 32-year-old woman received a complete HPV vaccine series at age 13. Cervical cytology 3 years ago was negative. She has no immunocompromise, in-utero diethylstilbestrol exposure, or prior high-grade lesion.",
|
||||
"Which statement best guides current screening?",
|
||||
["Continue age-appropriate cervical screening despite vaccination", "Stop screening because vaccination eliminates risk", "Perform annual endometrial biopsy", "Screen only if symptoms develop", "Begin ovarian cancer screening instead"],
|
||||
"HPV vaccination substantially reduces risk but does not eliminate all oncogenic HPV exposure or replace routine cervical screening schedules.",
|
||||
"HPV-vaccinated patients still need routine age-appropriate cervical cancer screening.", "complete HPV vaccine series", "cytology 3 years ago was negative", "no immunocompromise", "USPSTF", "Preventive Medicine & Health Maintenance"),
|
||||
|
||||
Q("falls-prevention-exercise", "Preventive Medicine & Health Maintenance", "Geriatrics", "fall prevention",
|
||||
"A 76-year-old woman living independently fell twice this year without injury. Vision and neurologic examination are normal. She takes no sedatives, but gait testing shows impaired balance and reduced leg strength.",
|
||||
"Which intervention has the best evidence for prevention?",
|
||||
["A structured exercise program emphasizing balance and strength", "Routine hip radiographs", "Prophylactic bed rest", "Daily benzodiazepine", "A wheelchair for all mobility"],
|
||||
"Exercise interventions that target balance, gait, and strength reduce falls in community-dwelling older adults at increased risk.",
|
||||
"Use structured balance and strength exercise for older adults at increased fall risk.", "impaired balance and reduced leg strength", "fell twice this year", "vision and neurologic examination are normal", "USPSTF", "Preventive Medicine & Health Maintenance"),
|
||||
|
||||
Q("migraine-aura-contraception", "Female Reproductive System and Breast", "Gynecology", "contraception migraine aura",
|
||||
"A 27-year-old woman with migraine with visual aura requests contraception. She does not smoke and has normal blood pressure. She prefers a highly effective reversible method and has no contraindication to an intrauterine device.",
|
||||
"Which option is most appropriate?",
|
||||
["Levonorgestrel intrauterine device", "Combined estrogen-progestin pill", "Combined hormonal patch", "Combined vaginal ring", "High-dose estrogen injection"],
|
||||
"Migraine with aura makes combined hormonal contraception an unacceptable stroke risk. A levonorgestrel IUD is highly effective and does not carry the estrogen-related contraindication.",
|
||||
"Avoid estrogen-containing contraception in migraine with aura; offer an effective nonestrogen method.", "migraine with visual aura", "does not smoke", "normal blood pressure", "CDC-MEC", "Renal/Urinary & Male/Female Systems & Pregnancy"),
|
||||
|
||||
Q("postpartum-endometritis", "Female Reproductive System and Breast", "Obstetrics", "postpartum endometritis",
|
||||
"Two days after an unplanned cesarean delivery, a 31-year-old woman has fever, uterine fundal tenderness, and foul-smelling lochia. The incision is clean, urinalysis is normal, and there is no calf tenderness.",
|
||||
"Which treatment is most appropriate?",
|
||||
["Intravenous clindamycin plus gentamicin", "Oral fluconazole", "Incision drainage despite a clean wound", "Anticoagulation alone", "Observation without antibiotics"],
|
||||
"Postcesarean fever with uterine tenderness and malodorous lochia indicates polymicrobial postpartum endometritis. Broad intravenous anaerobic and gram-negative coverage is required.",
|
||||
"Treat postpartum endometritis with broad-spectrum intravenous antibiotics covering anaerobes and gram-negative organisms.", "uterine fundal tenderness", "foul-smelling lochia", "incision is clean", "ACOG", "Renal/Urinary & Male/Female Systems & Pregnancy"),
|
||||
|
||||
Q("ectopic-pregnancy-methotrexate-failure", "Female Reproductive System and Breast", "Obstetrics", "ectopic pregnancy",
|
||||
"A hemodynamically stable woman receives methotrexate for a small unruptured tubal ectopic pregnancy. Her beta-hCG level falls 3% between treatment days 4 and 7. She has mild unchanged pelvic discomfort and no peritoneal signs.",
|
||||
"Which is the most appropriate next step?",
|
||||
["Administer an additional methotrexate dose or proceed with surgical management", "Declare treatment successful and stop follow-up", "Repeat beta-hCG in one year", "Begin progesterone supplementation", "Perform uterine curettage as definitive tubal treatment"],
|
||||
"A decline of less than 15% from days 4 to 7 indicates inadequate response. Additional methotrexate or surgery is needed, with continued surveillance until resolution.",
|
||||
"After methotrexate for ectopic pregnancy, a day-4-to-7 hCG decline below 15% requires further treatment.", "falls 3% between treatment days 4 and 7", "hemodynamically stable", "no peritoneal signs", "ACOG", "Renal/Urinary & Male/Female Systems & Pregnancy"),
|
||||
|
||||
Q("preeclampsia-severe-delivery", "Female Reproductive System and Breast", "Obstetrics", "preeclampsia severe features",
|
||||
"A woman at 36 weeks has blood pressure of 168/112 mm Hg on repeated measurements, new right upper-quadrant pain, platelets of 82,000/mm3, and elevated aminotransferases. Fetal testing is reassuring.",
|
||||
"Which management is most appropriate after stabilization?",
|
||||
["Magnesium sulfate, acute blood-pressure treatment, and delivery", "Expectant outpatient management until 40 weeks", "Tocolysis and discharge", "Aspirin as sole treatment", "Repeat laboratory testing in one month"],
|
||||
"Severe hypertension, thrombocytopenia, and hepatic symptoms establish preeclampsia with severe features. At 36 weeks, stabilize the mother, prevent seizure, and deliver.",
|
||||
"Preeclampsia with severe features at or beyond 34 weeks generally requires maternal stabilization followed by delivery.", "platelets of 82,000/mm3", "36 weeks", "fetal testing is reassuring", "ACOG", "Renal/Urinary & Male/Female Systems & Pregnancy"),
|
||||
|
||||
Q("shoulder-dystocia-mcRoberts", "Female Reproductive System and Breast", "Obstetrics", "shoulder dystocia",
|
||||
"During vaginal delivery, the fetal head delivers and retracts against the perineum. Gentle downward traction does not deliver the shoulders. The mother has an epidural, fetal heart rate is 90/min, and an assistant is present.",
|
||||
"Which maneuver should be performed first?",
|
||||
["McRoberts positioning with suprapubic pressure", "Fundal pressure", "Immediate cesarean delivery", "Forceful traction on the fetal head", "Vacuum extraction of the delivered head"],
|
||||
"The turtle sign indicates shoulder dystocia. Initial maneuvers are maternal hip hyperflexion and suprapubic pressure; fundal pressure and forceful traction worsen impaction and injury.",
|
||||
"Begin shoulder dystocia management with McRoberts positioning and suprapubic pressure.", "head delivers and retracts", "assistant is present", "mother has an epidural", "ACOG", "Renal/Urinary & Male/Female Systems & Pregnancy"),
|
||||
|
||||
Q("postmenopausal-bleeding-biopsy", "Female Reproductive System and Breast", "Gynecology", "postmenopausal bleeding",
|
||||
"A 63-year-old woman with obesity and diabetes reports new painless vaginal bleeding 11 years after menopause. Pelvic examination shows no cervical lesion. She uses no hormone therapy and is hemodynamically stable.",
|
||||
"Which evaluation is most appropriate?",
|
||||
["Endometrial assessment with transvaginal ultrasonography or biopsy", "Reassurance that bleeding is expected", "Serum CA-125 alone", "Empiric antibiotics", "Repeat examination in one year"],
|
||||
"Any postmenopausal bleeding requires prompt evaluation for endometrial cancer. Transvaginal ultrasonography or endometrial sampling is an appropriate initial approach depending on context.",
|
||||
"Evaluate postmenopausal bleeding promptly for endometrial neoplasia.", "new painless vaginal bleeding 11 years after menopause", "obesity and diabetes", "no cervical lesion", "ACOG", "Renal/Urinary & Male/Female Systems & Pregnancy"),
|
||||
|
||||
Q("torsion-surgery", "Female Reproductive System and Breast", "Gynecology", "ovarian torsion",
|
||||
"A 24-year-old woman develops sudden severe unilateral pelvic pain with vomiting. Ultrasonography shows an enlarged ovary with peripheral follicles; Doppler demonstrates some arterial flow. Pregnancy testing is negative and pain persists despite analgesia.",
|
||||
"Which is the most appropriate next step?",
|
||||
["Urgent diagnostic laparoscopy with detorsion", "Discharge because arterial flow excludes torsion", "Repeat ultrasonography in 6 months", "Methotrexate", "Endometrial biopsy"],
|
||||
"Torsion is a clinical and surgical diagnosis. Preserved Doppler arterial flow does not exclude intermittent or partial torsion; prompt laparoscopy maximizes ovarian salvage.",
|
||||
"Do not exclude ovarian torsion because Doppler flow persists; operate urgently when suspicion is high.", "sudden severe unilateral pelvic pain", "some arterial flow", "pregnancy testing is negative", "ACOG", "Renal/Urinary & Male/Female Systems & Pregnancy"),
|
||||
|
||||
Q("pelvic-inflammatory-disease-outpatient", "Female Reproductive System and Breast", "Gynecology", "pelvic inflammatory disease",
|
||||
"A 22-year-old woman has bilateral lower abdominal pain, mucopurulent cervical discharge, and cervical motion tenderness. Pregnancy testing is negative. She is afebrile, tolerates oral medication, and has no tubo-ovarian abscess.",
|
||||
"Which treatment is most appropriate?",
|
||||
["Outpatient ceftriaxone, doxycycline, and metronidazole", "Wait for gonorrhea culture confirmation", "Fluconazole alone", "Immediate hysterectomy", "Azithromycin single dose alone"],
|
||||
"Minimum clinical criteria justify empiric PID treatment to prevent infertility. Stable patients can receive recommended broad outpatient coverage for gonorrhea, chlamydia, and anaerobes.",
|
||||
"Treat suspected PID empirically when minimum clinical criteria are met; do not wait for microbiologic confirmation.", "cervical motion tenderness", "tolerates oral medication", "pregnancy testing is negative", "CDC-STI", "Renal/Urinary & Male/Female Systems & Pregnancy"),
|
||||
|
||||
Q("sudden-sensorineural-hearing-loss", "Ear, Nose & Throat", "Otolaryngology", "sudden hearing loss",
|
||||
"A 47-year-old man awakens with marked hearing loss in the left ear and tinnitus. Otoscopic examination is normal. Weber testing lateralizes to the right and Rinne testing shows air conduction greater than bone conduction bilaterally. Symptoms began 18 hours ago.",
|
||||
"Which is the most appropriate next step?",
|
||||
["Urgent corticosteroid treatment and audiologic evaluation", "Cerumen irrigation", "Routine follow-up in 3 months", "Oral antibiotics for otitis media", "Reassurance that normal otoscopy excludes urgency"],
|
||||
"Tuning-fork findings indicate sudden sensorineural hearing loss, an otologic emergency. Prompt corticosteroid therapy and audiometry improve the chance of recovery.",
|
||||
"Treat sudden sensorineural hearing loss urgently; a normal ear canal and tympanic membrane do not make it benign.", "Weber testing lateralizes to the right", "otoscopic examination is normal", "symptoms began 18 hours ago", "AAO-HNS", "Nervous System & Special Senses"),
|
||||
|
||||
Q("peritonsillar-abscess-drainage", "Ear, Nose & Throat", "Emergency Medicine", "peritonsillar abscess",
|
||||
"A 20-year-old man has fever, severe unilateral throat pain, drooling, muffled voice, trismus, and deviation of the uvula away from a swollen tonsillar pillar. He has no stridor and can sit upright.",
|
||||
"Which management is most appropriate?",
|
||||
["Drain the abscess and give antibiotics", "Rapid streptococcal testing alone", "Discharge with an antihistamine", "Immediate tonsil radiation", "Observe without airway assessment"],
|
||||
"Trismus, muffled voice, unilateral swelling, and uvular deviation indicate peritonsillar abscess. Treatment requires drainage plus antimicrobial therapy while monitoring the airway.",
|
||||
"Peritonsillar abscess requires source control and antibiotics, with airway assessment first.", "deviation of the uvula", "has no stridor", "can sit upright", "AAO-HNS", "Nervous System & Special Senses"),
|
||||
|
||||
Q("epistaxis-posterior", "Ear, Nose & Throat", "Emergency Medicine", "posterior epistaxis",
|
||||
"A 74-year-old man taking apixaban has brisk nasal bleeding into the posterior pharynx. Firm compression and topical vasoconstrictor fail. No anterior bleeding point is visible, blood pressure is stable, and large clots continue to enter the throat.",
|
||||
"Which is the most appropriate next step?",
|
||||
["Posterior nasal packing with monitored otolaryngology management", "Repeated nose blowing", "Discharge with petroleum jelly", "Tilt the head backward and swallow blood", "Elective sinus CT in one month"],
|
||||
"Persistent bleeding into the pharynx without an anterior source suggests posterior epistaxis. Posterior packing or procedural control and monitored specialty care are necessary because airway and hemodynamic complications can occur.",
|
||||
"Escalate suspected posterior epistaxis to packing or procedural control in a monitored setting.", "large clots continue to enter the throat", "blood pressure is stable", "no anterior bleeding point is visible", "AAO-HNS", "Nervous System & Special Senses"),
|
||||
|
||||
Q("deep-neck-infection-airway", "Ear, Nose & Throat", "Emergency Medicine", "deep neck infection",
|
||||
"A 39-year-old man with a mandibular molar infection develops bilateral submandibular swelling, tongue elevation, drooling, and a muffled voice. He is sitting forward and oxygen saturation is 94% on room air. Neck examination shows woody induration.",
|
||||
"Which action has highest priority?",
|
||||
["Secure the airway with expert assistance before deterioration", "Send him home with oral mouthwash", "Perform blind oral intubation after sedation", "Wait for fluctuance before treatment", "Obtain dental radiographs before airway planning"],
|
||||
"Ludwig angina can rapidly obstruct the airway. Early controlled airway management with anesthesia and otolaryngology support precedes antibiotics and surgical source control when needed.",
|
||||
"In a deep neck infection with tongue elevation and drooling, secure the airway early under controlled conditions.", "tongue elevation", "oxygen saturation is 94%", "woody induration", "AAO-HNS", "Nervous System & Special Senses"),
|
||||
|
||||
Q("malignant-otitis-externa", "Ear, Nose & Throat", "Medicine", "necrotizing otitis externa",
|
||||
"A 71-year-old man with poorly controlled diabetes has severe nocturnal ear pain and purulent drainage despite topical therapy. Granulation tissue is present at the bony-cartilaginous junction of the canal. He is afebrile and the tympanic membrane is intact.",
|
||||
"Which management is most appropriate?",
|
||||
["Systemic antipseudomonal therapy and urgent specialty evaluation", "Topical anesthetic drops alone", "Routine cerumen removal and discharge", "Oral amoxicillin for 3 days", "Observation because he is afebrile"],
|
||||
"Severe refractory otalgia, diabetes, and canal granulation tissue suggest invasive Pseudomonas infection of the skull base. Prolonged systemic antipseudomonal treatment and ENT assessment are required.",
|
||||
"Suspect necrotizing otitis externa in a diabetic patient with severe persistent pain and canal granulation tissue.", "granulation tissue", "poorly controlled diabetes", "tympanic membrane is intact", "AAO-HNS", "Nervous System & Special Senses"),
|
||||
|
||||
Q("anaphylaxis-epinephrine", "Immune System", "Emergency Medicine", "anaphylaxis",
|
||||
"Minutes after eating a cookie, a 17-year-old develops diffuse urticaria, wheezing, vomiting, and blood pressure of 78/42 mm Hg. The airway is currently patent. Intravenous access has not yet been obtained.",
|
||||
"Which medication should be administered first?",
|
||||
["Intramuscular epinephrine in the lateral thigh", "Intravenous diphenhydramine", "Nebulized albuterol alone", "Intravenous glucocorticoid", "Subcutaneous epinephrine after IV access"],
|
||||
"Multisystem allergic symptoms with hypotension are anaphylaxis. Intramuscular epinephrine is the immediate first-line treatment and should not be delayed for venous access or adjuncts.",
|
||||
"Give intramuscular epinephrine immediately for anaphylaxis; antihistamines and steroids are adjuncts.", "blood pressure of 78/42 mm Hg", "airway is currently patent", "intravenous access has not yet been obtained", "AAAAI", "Immune/Blood & Lymphoreticular/Endocrine/Multisystem"),
|
||||
|
||||
Q("ace-inhibitor-angioedema-airway", "Immune System", "Emergency Medicine", "angioedema",
|
||||
"A 64-year-old man taking lisinopril develops progressive tongue and floor-of-mouth swelling without urticaria. His voice is muffled, he cannot handle secretions, and oxygen saturation remains 97% on room air.",
|
||||
"Which action is most important?",
|
||||
["Early definitive airway management and discontinuation of lisinopril", "Wait for oxygen desaturation", "Give an oral antihistamine and discharge", "Continue lisinopril after swelling resolves", "Perform skin-prick testing before treatment"],
|
||||
"Bradykinin-mediated ACE inhibitor angioedema often lacks urticaria and may progress rapidly. Inability to handle secretions and voice change require early controlled airway protection.",
|
||||
"In ACE inhibitor angioedema, airway findings—not oxygen saturation alone—determine the need for early airway control.", "cannot handle secretions", "without urticaria", "oxygen saturation remains 97%", "AAAAI", "Immune/Blood & Lymphoreticular/Endocrine/Multisystem"),
|
||||
|
||||
Q("common-variable-immunodeficiency", "Immune System", "Medicine", "primary immunodeficiency",
|
||||
"A 28-year-old woman has recurrent bacterial sinusitis and two episodes of pneumonia. IgG and IgA levels are markedly reduced, vaccine antibody responses are poor, and absolute B-cell count is normal. HIV testing is negative.",
|
||||
"Which long-term treatment is most appropriate?",
|
||||
["Immunoglobulin replacement therapy", "Daily systemic glucocorticoids", "Splenectomy", "Live vaccines monthly", "No treatment because B cells are present"],
|
||||
"Low immunoglobulins and impaired specific antibody response with preserved B-cell numbers support common variable immunodeficiency. Immunoglobulin replacement reduces serious bacterial infections.",
|
||||
"Treat clinically significant antibody deficiency in common variable immunodeficiency with immunoglobulin replacement.", "vaccine antibody responses are poor", "absolute B-cell count is normal", "HIV testing is negative", "AAAAI", "Immune/Blood & Lymphoreticular/Endocrine/Multisystem"),
|
||||
|
||||
Q("hereditary-angioedema", "Immune System", "Medicine", "hereditary angioedema",
|
||||
"A 23-year-old man has recurrent nonpruritic swelling of the hands, severe episodic abdominal pain, and a family history of similar attacks. During episodes he has no urticaria. Complement testing shows persistently low C4.",
|
||||
"Which treatment is appropriate for an acute laryngeal attack?",
|
||||
["C1 esterase inhibitor concentrate", "Diphenhydramine alone", "High-dose aspirin", "Epinephrine as the only definitive therapy", "Oral antibiotics"],
|
||||
"Recurrent angioedema without hives, abdominal attacks, family history, and low C4 indicate hereditary C1 inhibitor deficiency. Acute attacks require targeted bradykinin-pathway therapy such as C1 inhibitor concentrate.",
|
||||
"Hereditary angioedema is bradykinin-mediated and requires targeted on-demand therapy, especially for laryngeal attacks.", "persistently low C4", "no urticaria", "family history of similar attacks", "AAAAI", "Immune/Blood & Lymphoreticular/Endocrine/Multisystem"),
|
||||
|
||||
Q("serum-sickness-like", "Immune System", "Pediatrics", "serum sickness",
|
||||
"A 9-year-old boy develops fever, urticarial eruption, arthralgias, and lymphadenopathy 9 days after starting cefaclor. Urinalysis shows mild proteinuria, complement levels are low, and there is no mucosal erosion.",
|
||||
"Which is the most appropriate initial management?",
|
||||
["Discontinue the triggering drug and provide supportive therapy", "Continue cefaclor to complete the course", "Administer epinephrine for isolated delayed symptoms", "Begin lifelong immunoglobulin replacement", "Perform emergent skin debridement"],
|
||||
"The delayed fever, rash, arthralgia, and hypocomplementemia indicate an immune-complex serum sickness reaction. Removing the antigen and supportive therapy are primary; severe cases may need systemic steroids.",
|
||||
"For serum sickness, stop the causative exposure and treat symptomatically while assessing organ involvement.", "9 days after starting cefaclor", "complement levels are low", "no mucosal erosion", "AAAAI", "Immune/Blood & Lymphoreticular/Endocrine/Multisystem"),
|
||||
|
||||
Q("wrong-patient-near-miss", "Social Sciences and Patient Safety", "Patient Safety", "near miss analysis",
|
||||
"A nurse scans a medication and receives a wrong-patient alert before administration. The two patients have the same surname and were placed in adjacent rooms; no drug reaches either patient. Staff routinely bypass a second identifier during busy shifts.",
|
||||
"Which response best improves safety?",
|
||||
["Report the near miss and analyze system causes including identifier workflow", "Take no action because no injury occurred", "Discipline the nurse without reviewing the process", "Disable barcode alerts", "Move only these two patients and close the review"],
|
||||
"Near misses reveal hazards before harm occurs. Reporting and systems analysis can address room assignment, identifier use, alert design, staffing, and workarounds rather than relying on individual blame.",
|
||||
"Analyze near misses for latent system defects even when the safety barrier prevents harm.", "wrong-patient alert before administration", "no drug reaches either patient", "routinely bypass a second identifier", "AHRQ"),
|
||||
|
||||
Q("root-cause-analysis", "Social Sciences and Patient Safety", "Patient Safety", "root cause analysis",
|
||||
"A patient receives a concentrated electrolyte instead of a flush and has a cardiac arrest. The vials had similar labels, were stored together, and the electronic order did not require pharmacy verification. Multiple staff followed the same usual workflow.",
|
||||
"Which corrective action is most likely to prevent recurrence?",
|
||||
["Redesign storage, labeling, and verification barriers through a root cause analysis", "Retrain the last nurse only", "Ask staff to be more careful", "Keep the vials together for convenience", "Suppress incident reporting"],
|
||||
"The event reflects several latent system hazards. Root cause analysis should produce strong safeguards such as segregation, standardization, forcing functions, and independent verification.",
|
||||
"Use systems analysis and strong process controls rather than education or blame alone after a serious safety event.", "vials had similar labels", "stored together", "multiple staff followed the same usual workflow", "AHRQ"),
|
||||
|
||||
Q("central-line-bundle", "Social Sciences and Patient Safety", "Patient Safety", "infection prevention bundle",
|
||||
"An ICU has a rising central-line bloodstream infection rate. Audits show inconsistent chlorhexidine skin preparation, unnecessary femoral lines, and no daily review of line necessity. Individual clinicians use different insertion practices.",
|
||||
"Which intervention is most effective?",
|
||||
["Implement a standardized insertion and maintenance bundle with daily necessity review", "Treat every patient with prophylactic antibiotics", "Replace every line daily", "Publish clinician names without process changes", "Culture all line tips weekly"],
|
||||
"Central-line bundles combine hand hygiene, maximal barrier precautions, chlorhexidine, site selection, maintenance, and prompt removal. Standardized reliable execution addresses the observed process defects.",
|
||||
"Prevent central-line infection through a consistently applied bundle and removal of unnecessary lines.", "no daily review of line necessity", "inconsistent chlorhexidine skin preparation", "individual clinicians use different insertion practices", "AHRQ"),
|
||||
|
||||
Q("handoff-readback", "Social Sciences and Patient Safety", "Patient Safety", "closed-loop communication",
|
||||
"During a chaotic trauma resuscitation, the team leader requests 1 unit of uncrossmatched blood. Two nurses hear different quantities, and neither repeats the order. Similar verbal-order misunderstandings have occurred during prior resuscitations.",
|
||||
"Which change most directly addresses this hazard?",
|
||||
["Require closed-loop communication with read-back of critical verbal orders", "Ban all speech during resuscitation", "Add a longer incident form", "Ask only senior staff to listen", "Delay all treatment until orders are typed"],
|
||||
"Closed-loop communication requires the receiver to repeat the order and the sender to confirm it, catching misheard critical instructions in real time.",
|
||||
"Use read-back and confirmation for critical verbal orders in high-risk settings.", "neither repeats the order", "two nurses hear different quantities", "chaotic trauma resuscitation", "AHRQ"),
|
||||
|
||||
Q("control-chart-special-cause", "Social Sciences and Patient Safety", "Quality Improvement", "statistical process control",
|
||||
"A clinic plots monthly missed-result rates on a control chart. After a new inbox system, eight consecutive points fall below the previous centerline, although every point remains within the old control limits. Patient volume is unchanged.",
|
||||
"How should this pattern be interpreted?",
|
||||
["A nonrandom shift suggests special-cause improvement", "No change occurred because limits were not crossed", "The process necessarily became less stable", "The sample must be discarded", "The centerline can never be recalculated"],
|
||||
"A sustained run on one side of the centerline is a standard nonrandom signal even without a point beyond control limits. The timing supports a process shift associated with the intervention.",
|
||||
"Control charts detect nonrandom runs and shifts, not only points outside control limits.", "eight consecutive points fall below", "within the old control limits", "patient volume is unchanged", "AHRQ"),
|
||||
|
||||
Q("language-interpreter", "Social Sciences and Patient Safety", "Communication", "medical interpretation",
|
||||
"A patient with limited English proficiency is considering high-risk surgery. His adult daughter offers to interpret, but she omits discussion of infertility because it is embarrassing. A qualified video interpreter is immediately available.",
|
||||
"Which is the best approach?",
|
||||
["Use the qualified medical interpreter for informed consent", "Use the daughter because she is family", "Obtain a signature without further discussion", "Cancel surgery permanently", "Ask another patient to interpret"],
|
||||
"Meaningful informed consent requires accurate, complete communication. A qualified interpreter reduces omissions and conflicts while family may participate at the patient's request.",
|
||||
"Use a qualified medical interpreter for consequential clinical communication and informed consent.", "omits discussion of infertility", "qualified video interpreter is immediately available", "adult daughter offers to interpret", "AHRQ"),
|
||||
|
||||
Q("thyroid-storm-treatment", "Endocrine System", "Medicine", "thyroid storm",
|
||||
"A 36-year-old woman with untreated Graves disease has fever of 40 C, agitation, vomiting, atrial fibrillation at 170/min, and heart failure after pneumonia. She is receiving supportive resuscitation and has no contraindication to beta blockade.",
|
||||
"Which medication sequence is appropriate?",
|
||||
["Beta blocker, thionamide, then iodine after thionamide, plus glucocorticoid", "Iodine before thionamide", "Levothyroxine and aspirin", "Radioactive iodine as immediate monotherapy", "Observation until thyroid tests return"],
|
||||
"Thyroid storm requires rapid multimodal therapy. Block adrenergic effects and synthesis first, then give iodine after thionamide to avoid providing substrate for new hormone production; steroids reduce conversion and treat possible adrenal insufficiency.",
|
||||
"In thyroid storm, give iodine only after a thionamide, along with beta blockade, steroids, and supportive care.", "fever of 40 C", "untreated Graves disease", "after pneumonia", "ENDO", "Immune/Blood & Lymphoreticular/Endocrine/Multisystem"),
|
||||
|
||||
Q("adrenal-crisis", "Endocrine System", "Medicine", "adrenal crisis",
|
||||
"A 45-year-old woman with autoimmune adrenal insufficiency presents with vomiting, confusion, blood pressure of 72/40 mm Hg, sodium of 124 mEq/L, potassium of 6.1 mEq/L, and glucose of 48 mg/dL after gastroenteritis.",
|
||||
"Which treatment should begin immediately?",
|
||||
["Intravenous hydrocortisone and isotonic saline with dextrose", "Wait for cosyntropin testing", "Fluid restriction", "Insulin without glucose", "Oral fludrocortisone alone"],
|
||||
"Shock, hypoglycemia, hyponatremia, and hyperkalemia in known adrenal insufficiency indicate adrenal crisis. Stress-dose hydrocortisone and rapid volume and glucose replacement must not await testing.",
|
||||
"Treat suspected adrenal crisis immediately with parenteral glucocorticoid and volume resuscitation.", "blood pressure of 72/40 mm Hg", "glucose of 48 mg/dL", "after gastroenteritis", "ENDO", "Immune/Blood & Lymphoreticular/Endocrine/Multisystem"),
|
||||
|
||||
Q("pheochromocytoma-alpha-before-beta", "Endocrine System", "Surgery", "pheochromocytoma preparation",
|
||||
"A 42-year-old woman has episodic headache, diaphoresis, palpitations, and an adrenal mass with elevated plasma metanephrines. Resection is planned. Blood pressure is 176/104 mm Hg and pulse is 112/min.",
|
||||
"Which preoperative strategy is appropriate?",
|
||||
["Establish alpha blockade before adding beta blockade", "Begin propranolol before alpha blockade", "Proceed directly to surgery without preparation", "Restrict salt and fluid severely", "Treat with levothyroxine"],
|
||||
"Alpha blockade and volume expansion prepare patients for resection. Giving beta blockade first can leave unopposed alpha stimulation and precipitate hypertensive crisis.",
|
||||
"For pheochromocytoma, establish adequate alpha blockade before treating residual tachycardia with a beta blocker.", "elevated plasma metanephrines", "blood pressure is 176/104 mm Hg", "resection is planned", "ENDO", "Immune/Blood & Lymphoreticular/Endocrine/Multisystem"),
|
||||
|
||||
Q("hypercalcemic-crisis-fluids", "Endocrine System", "Medicine", "severe hypercalcemia",
|
||||
"A 69-year-old man with metastatic squamous cell carcinoma has confusion, constipation, polyuria, corrected calcium of 15.8 mg/dL, and creatinine of 2.0 mg/dL. Mucous membranes are dry and lungs are clear.",
|
||||
"Which treatment should be initiated first?",
|
||||
["Aggressive isotonic saline followed by rapid antiresorptive therapy", "Thiazide diuretic", "Fluid restriction", "Oral calcium supplementation", "Observation until parathyroid hormone returns"],
|
||||
"Severe symptomatic hypercalcemia causes volume depletion. Isotonic saline restores perfusion, while calcitonin and an intravenous bisphosphonate or denosumab lower calcium based on context.",
|
||||
"Begin treatment of severe symptomatic hypercalcemia with isotonic volume expansion and prompt calcium-lowering therapy.", "corrected calcium of 15.8 mg/dL", "mucous membranes are dry", "lungs are clear", "ENDO", "Immune/Blood & Lymphoreticular/Endocrine/Multisystem"),
|
||||
|
||||
Q("testicular-torsion-exploration", "Male Reproductive System", "Urology", "testicular torsion",
|
||||
"A 15-year-old boy has sudden severe left scrotal pain, vomiting, a high-riding horizontal testis, and an absent cremasteric reflex. Symptoms began 3 hours ago. The operating room is available immediately.",
|
||||
"Which is the most appropriate next step?",
|
||||
["Immediate surgical exploration and bilateral orchiopexy", "Wait for outpatient ultrasonography", "Treat epididymitis with oral antibiotics", "Needle aspiration", "Reassess after 24 hours"],
|
||||
"The classic presentation has high pretest probability for torsion. Surgical exploration must not be delayed for imaging, and the contralateral testis is fixed because the anatomic predisposition is usually bilateral.",
|
||||
"When testicular torsion is clinically clear, proceed directly to urgent exploration and bilateral fixation.", "absent cremasteric reflex", "symptoms began 3 hours ago", "operating room is available immediately", "AUA", "Renal/Urinary & Male/Female Systems & Pregnancy"),
|
||||
|
||||
Q("priapism-aspiration", "Male Reproductive System", "Urology", "ischemic priapism",
|
||||
"A 38-year-old man with sickle cell disease has a painful rigid erection for 6 hours. The corpora cavernosa are hard, the glans is soft, and cavernosal blood gas shows acidosis and hypoxia. Hydration and analgesia have not produced detumescence.",
|
||||
"Which is the most appropriate next step?",
|
||||
["Corporal aspiration and irrigation with intracavernosal phenylephrine", "Observation until 24 hours", "Oral sildenafil", "Apply a warm compress only", "Elective penile Doppler next month"],
|
||||
"Painful low-flow priapism is a compartment syndrome of the penis. Aspiration, irrigation, and intracavernosal sympathomimetic therapy are urgent to prevent necrosis and erectile dysfunction.",
|
||||
"Treat ischemic priapism urgently with aspiration and intracavernosal phenylephrine.", "cavernosal blood gas shows acidosis and hypoxia", "sickle cell disease", "hydration and analgesia have not produced detumescence", "AUA", "Renal/Urinary & Male/Female Systems & Pregnancy"),
|
||||
|
||||
Q("sjs-ten-drug-withdrawal", "Skin and Subcutaneous Tissue", "Dermatology", "Stevens-Johnson syndrome",
|
||||
"A 35-year-old woman develops fever, painful dusky targetoid lesions, oral erosions, and conjunctivitis 10 days after starting lamotrigine. Epidermal detachment involves 8% of body surface area. Nikolsky sign is present.",
|
||||
"Which action is most important?",
|
||||
["Stop lamotrigine immediately and provide specialized supportive care", "Continue lamotrigine with an antihistamine", "Debride all detached epidermis at bedside", "Prescribe topical acne therapy", "Discharge after oral fluids"],
|
||||
"Mucosal erosions and epidermal detachment after a high-risk drug indicate Stevens-Johnson syndrome. Immediate withdrawal and burn-unit-level supportive care reduce ongoing injury and complications.",
|
||||
"Immediately stop the culprit drug in suspected SJS/TEN and provide intensive supportive care.", "oral erosions", "10 days after starting lamotrigine", "epidermal detachment involves 8%", "FDA-LAM", "Musculoskeletal/Skin & Subcutaneous Tissue"),
|
||||
|
||||
Q("necrotizing-fasciitis-surgery", "Skin and Subcutaneous Tissue", "Surgery", "necrotizing soft tissue infection",
|
||||
"A 56-year-old man with diabetes has rapidly spreading leg erythema, pain far out of proportion, hemorrhagic bullae, crepitus, and hypotension. Broad antibiotics and resuscitation are started. CT is not immediately available.",
|
||||
"Which is the most appropriate next step?",
|
||||
["Immediate surgical exploration and debridement", "Wait for CT confirmation", "Incision and drainage at the bedside only", "Topical antibiotics", "Observe response for 24 hours"],
|
||||
"Shock, disproportionate pain, bullae, and crepitus indicate necrotizing infection. Definitive diagnosis and source control require immediate exploration; imaging must not delay surgery.",
|
||||
"Suspected necrotizing soft tissue infection requires urgent operative exploration and debridement.", "pain far out of proportion", "CT is not immediately available", "broad antibiotics and resuscitation are started", "IDSA-SSTI", "Musculoskeletal/Skin & Subcutaneous Tissue")
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user