Restore distractor-specific follow-up questions
This commit is contained in:
@@ -92,6 +92,33 @@ public sealed class ChoicePoolTests
|
||||
Assert.Contains("every incorrect choice", QuestionValidator.Validate(request));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("prompt")]
|
||||
[InlineData("teaching")]
|
||||
[InlineData("correct")]
|
||||
[InlineData("choices")]
|
||||
public void PublishedQuestionRejectsClonedDistractorFollowups(string duplicate)
|
||||
{
|
||||
var request = BuildRequest(5);
|
||||
var first = request.WrongAnswerFollowups["B"];
|
||||
var second = request.WrongAnswerFollowups["C"];
|
||||
if (duplicate == "prompt") second.Prompt = first.Prompt;
|
||||
if (duplicate == "teaching") second.TeachingPoint = first.TeachingPoint;
|
||||
if (duplicate == "correct") second.Choices.Single(x => x.Id == second.CorrectChoiceId).Text =
|
||||
first.Choices.Single(x => x.Id == first.CorrectChoiceId).Text;
|
||||
if (duplicate == "choices") second.Choices = first.Choices.Select(x => new QuestionChoice { Id=x.Id, Text=x.Text }).ToList();
|
||||
|
||||
Assert.Contains("different", QuestionValidator.Validate(request));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PublishedQuestionRequiresThreeDistractorFollowupChoices()
|
||||
{
|
||||
var request = BuildRequest(5);
|
||||
request.WrongAnswerFollowups["B"].Choices.RemoveAt(2);
|
||||
Assert.Contains("exactly three", QuestionValidator.Validate(request));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OptionalReviewFollowupsRequireValidAuthoredContent()
|
||||
{
|
||||
@@ -275,14 +302,15 @@ public sealed class ChoicePoolTests
|
||||
id => id,
|
||||
id => new WrongAnswerFollowup
|
||||
{
|
||||
Prompt = "Which finding makes this less appropriate?",
|
||||
Prompt = $"Which property of Choice {id} makes it less appropriate?",
|
||||
Choices =
|
||||
[
|
||||
new QuestionChoice { Id = "A", Text = "Finding one" },
|
||||
new QuestionChoice { Id = "B", Text = "Finding two" }
|
||||
new QuestionChoice { Id = "A", Text = $"Disqualifying property of Choice {id}" },
|
||||
new QuestionChoice { Id = "B", Text = $"Plausible property one for Choice {id}" },
|
||||
new QuestionChoice { Id = "C", Text = $"Plausible property two for Choice {id}" }
|
||||
],
|
||||
CorrectChoiceId = "A",
|
||||
TeachingPoint = "Teaching point"
|
||||
TeachingPoint = $"Choice {id} fails because of its specific disqualifying property."
|
||||
}),
|
||||
ChoiceSelectionPercentages = ids.ToDictionary(id => id, _ => 0),
|
||||
Discipline = "Medicine",
|
||||
|
||||
@@ -57,6 +57,14 @@ least one embedded authoritative verification reference. Legacy published
|
||||
questions without references remain readable but must add references before a
|
||||
future authoring update.
|
||||
|
||||
Wrong-answer follow-ups are distractor-specific remediation, not generic review
|
||||
questions. Each distractor must have a different three-choice quiz about the
|
||||
mechanism, indication, contraindication, timing, or other property that makes
|
||||
that option inappropriate in the vignette. Its teaching point must explain why
|
||||
that distractor fails without revealing the main correct answer. Publication
|
||||
validation rejects duplicated prompts, correct follow-up answers, choice sets,
|
||||
and teaching points within a question.
|
||||
|
||||
- `GET /readyz` verifies MongoDB connectivity for readiness checks.
|
||||
- `GET /metrics` exposes Prometheus-format service and reconciliation metrics.
|
||||
|
||||
|
||||
@@ -283,7 +283,8 @@ public sealed class DynamicQuestionGenerator
|
||||
IReadOnlyDictionary<string, string> values)
|
||||
{
|
||||
var result = new Dictionary<string, WrongAnswerFollowup>();
|
||||
foreach (var distractor in distractors)
|
||||
var distractorList = distractors.ToList();
|
||||
foreach (var distractor in distractorList)
|
||||
{
|
||||
if (templates.TryGetValue(distractor.Id, out var template))
|
||||
{
|
||||
@@ -297,16 +298,18 @@ public sealed class DynamicQuestionGenerator
|
||||
};
|
||||
continue;
|
||||
}
|
||||
var alternatives = distractorList.Where(value => value.Id != distractor.Id).Take(2).ToArray();
|
||||
result[distractor.Id] = new WrongAnswerFollowup
|
||||
{
|
||||
Prompt = "Which option best addresses the decisive finding in this case?",
|
||||
Prompt = $"Which limitation of option {distractor.Id} should have ruled it out in this case?",
|
||||
Choices =
|
||||
[
|
||||
new QuestionChoice { Id = correct.Id, Text = Render(correct.TextTemplate, values) },
|
||||
new QuestionChoice { Id = distractor.Id, Text = Render(distractor.TextTemplate, values) }
|
||||
new QuestionChoice { Id = "A", Text = Render(distractor.ExplanationTemplate, values) },
|
||||
new QuestionChoice { Id = "B", Text = Render(alternatives[0].ExplanationTemplate, values) },
|
||||
new QuestionChoice { Id = "C", Text = Render(alternatives[1].ExplanationTemplate, values) }
|
||||
],
|
||||
CorrectChoiceId = correct.Id,
|
||||
TeachingPoint = $"{Render(distractor.ExplanationTemplate, values)} {Render(correct.ExplanationTemplate, values)}"
|
||||
CorrectChoiceId = "A",
|
||||
TeachingPoint = Render(distractor.ExplanationTemplate, values)
|
||||
};
|
||||
}
|
||||
return result;
|
||||
|
||||
@@ -49,6 +49,21 @@ public static class QuestionValidator
|
||||
if (!entry.Value.Choices.Any(choice => choice.Id == entry.Value.CorrectChoiceId))
|
||||
return "wrongAnswerFollowups correctChoiceId must match a follow-up choice id";
|
||||
}
|
||||
if (req.IsPublished)
|
||||
{
|
||||
if (req.WrongAnswerFollowups.Any(entry => entry.Value.Choices.Count != 3))
|
||||
return "published wrongAnswerFollowups require exactly three choices per distractor";
|
||||
if (!AllDistinct(req.WrongAnswerFollowups.Values.Select(value => value.Prompt)))
|
||||
return "published wrongAnswerFollowups require a different prompt for every distractor";
|
||||
if (!AllDistinct(req.WrongAnswerFollowups.Values.Select(value => value.TeachingPoint)))
|
||||
return "published wrongAnswerFollowups require a different teachingPoint for every distractor";
|
||||
if (!AllDistinct(req.WrongAnswerFollowups.Values.Select(value =>
|
||||
value.Choices.Single(choice => choice.Id == value.CorrectChoiceId).Text)))
|
||||
return "published wrongAnswerFollowups require a different correct follow-up answer for every distractor";
|
||||
if (!AllDistinct(req.WrongAnswerFollowups.Values.Select(value => string.Join("|",
|
||||
value.Choices.Select(choice => Normalize(choice.Text)).OrderBy(text => text, StringComparer.Ordinal)))))
|
||||
return "published wrongAnswerFollowups require a different choice set for every distractor";
|
||||
}
|
||||
|
||||
if (req.ReviewFollowups is null) return "reviewFollowups must be an array";
|
||||
if (req.ReviewFollowups.Count > 10) return "questions support at most ten review follow-ups";
|
||||
@@ -75,6 +90,16 @@ public static class QuestionValidator
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool AllDistinct(IEnumerable<string> values)
|
||||
{
|
||||
var normalized = values.Select(Normalize).ToList();
|
||||
return normalized.All(value => value.Length > 0)
|
||||
&& normalized.Distinct(StringComparer.Ordinal).Count() == normalized.Count;
|
||||
}
|
||||
|
||||
private static string Normalize(string value) => string.Join(' ', value.Trim().ToLowerInvariant()
|
||||
.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries));
|
||||
|
||||
public static string? ValidateLabs(IReadOnlyCollection<QuestionLab>? labs, bool requireComplete)
|
||||
{
|
||||
if (labs is null)
|
||||
|
||||
Reference in New Issue
Block a user