Upgrade dynamic questions to authored parity

This commit is contained in:
hz
2026-07-23 14:11:18 -05:00
parent e174b5db15
commit 92c5688b55
12 changed files with 690 additions and 18 deletions
@@ -48,6 +48,38 @@ public sealed class DynamicQuestionTests
Assert.ThrowsAny<Exception>(() => service.ReadGeneration(token + "x", "user-one"));
}
[Fact]
public void GoldGenerator_UsesCaseFamilies_AndProducesAuthoredTeachingShape()
{
var blueprint = GoldBlueprint();
var generator = new DynamicQuestionGenerator();
var generated = Enumerable.Range(0, 100)
.Select(seed => generator.Generate(blueprint, seed.ToString(), seed.ToString()))
.ToList();
Assert.Equal(3, generated.Select(value => value.CaseFamilyId).Distinct().Count());
Assert.All(generated, value =>
{
Assert.True(value.Question.Stem.Length >= 220);
Assert.Equal(5, value.Question.Choices.Count);
Assert.Equal(5, value.Question.ChoiceExplanations.Count);
Assert.Single(value.Question.ReviewFollowups);
Assert.Equal(4, value.Question.WrongAnswerFollowups.Count);
Assert.Contains("quality-standard:2", value.Question.Tags);
});
Assert.Null(DynamicQuestionGenerator.Validate(blueprint));
}
[Fact]
public void LegacyGeneratorVersion_RemainsDeterministic()
{
var generator = new DynamicQuestionGenerator();
var first = generator.Generate(Blueprint(), "legacy-seed", "one", 1).Question;
var regenerated = generator.Generate(Blueprint(), "legacy-seed", "two", 1).Question;
Assert.Equal(first.Stem, regenerated.Stem);
Assert.Equal(first.Choices.Select(value => value.Id), regenerated.Choices.Select(value => value.Id));
}
private static DynamicQuestionBlueprint Blueprint() => new()
{
ExternalId = "test",
@@ -68,4 +100,50 @@ public sealed class DynamicQuestionTests
],
References = [new QuestionReference { Title = "Reference", Url = "https://example.test" }]
};
private static DynamicQuestionBlueprint GoldBlueprint()
{
var stem = string.Join(" ", Enumerable.Repeat(
"A patient presents with a carefully correlated history, examination, vital signs, and diagnostic findings that establish the clinical context without naming the diagnosis.", 3));
return new DynamicQuestionBlueprint
{
ExternalId = "gold-test",
Version = 2,
ObjectiveId = "gold.objective",
QualityStandardVersion = 2,
CorrectConceptId = "gold.correct",
CorrectAnswer = new DynamicAnswerTemplate
{ Id = "correct", TextTemplate = "Correct action", ExplanationTemplate = "This addresses the decisive finding." },
Distractors = Enumerable.Range(1, 6).Select(index => new DynamicAnswerTemplate
{
Id = $"d{index}",
TextTemplate = $"Distractor {index}",
ExplanationTemplate = $"Distractor {index} does not address the decisive finding."
}).ToList(),
ExplanationTemplate = "A complete explanation connects the presentation to the action.",
KeyTakeaway = "Use the decisive finding.",
UsmlePearl = "Sequence urgent care correctly.",
ReviewFollowups =
[
new DynamicReviewFollowupTemplate
{
PromptTemplate = "What is decisive?",
Choices =
[
new() { Id = "yes", TextTemplate = "The unstable finding" },
new() { Id = "no", TextTemplate = "An incidental finding" }
],
CorrectChoiceId = "yes",
TeachingPointTemplate = "The unstable finding determines management."
}
],
CaseFamilies =
[
new() { Id = "family-a", StemTemplate = stem, LeadIn = "What is next?" },
new() { Id = "family-b", StemTemplate = stem + " Family B.", LeadIn = "What is next?" },
new() { Id = "family-c", StemTemplate = stem + " Family C.", LeadIn = "What is next?" }
],
References = [new QuestionReference { Title = "Reference", Url = "https://example.test" }]
};
}
}