Restore distractor-specific follow-up questions

This commit is contained in:
hz
2026-08-01 18:06:52 -05:00
parent 8c5aa17d89
commit c7f5006cb6
14 changed files with 76770 additions and 100 deletions
@@ -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)