diff --git a/backend/migrations-active/20260826000000-repair-lesson-43-context-gap.cjs b/backend/migrations-active/20260826000000-repair-lesson-43-context-gap.cjs new file mode 100644 index 0000000..65f92d0 --- /dev/null +++ b/backend/migrations-active/20260826000000-repair-lesson-43-context-gap.cjs @@ -0,0 +1,28 @@ +'use strict'; + +const LESSON_TITLE = 'Kultur: Höflichkeit, Familie, Alltag'; + +module.exports = { + async up(queryInterface) { + await queryInterface.sequelize.transaction(async (transaction) => { + // The old task asked for a soft refusal to an invitation, but expected + // the unrelated cultural keyword "respeto". Test the matching phrase. + await queryInterface.sequelize.query(` + UPDATE community.vocab_grammar_exercise AS exercise + SET question_data = '{"type":"gap_fill","text":"{gap}","gaps":1}'::jsonb, + answer_data = '{"type":"gap_fill","answers":["Dili lang sa karon."]}'::jsonb, + explanation = 'Das Kernmuster lautet: "Dili lang sa karon."' + FROM community.vocab_course_lesson AS lesson + WHERE exercise.lesson_id = lesson.id + AND (lesson.id = 40 OR (lesson.course_id = 1 AND lesson.lesson_number = 43)) + AND exercise.title = :lessonTitle || ': Kernmuster ergänzen' + AND exercise.question_data->>'type' = 'gap_fill' + AND exercise.answer_data->'answers' @> '["respeto"]'::jsonb; + `, { replacements: { lessonTitle: LESSON_TITLE }, transaction }); + }); + }, + + async down() { + // Do not restore the semantically mismatched task. + } +}; diff --git a/backend/scripts/create-bisaya-course-content.js b/backend/scripts/create-bisaya-course-content.js index 2e079ec..6303cac 100755 --- a/backend/scripts/create-bisaya-course-content.js +++ b/backend/scripts/create-bisaya-course-content.js @@ -598,7 +598,24 @@ function buildGapExercise(lessonTitle, pattern) { } function buildContextGapExercise(lesson, didactics, pattern) { - const gapExercise = buildGapExercise(lesson.title, pattern); + const speakingPrompt = Array.isArray(didactics.speakingPrompts) ? didactics.speakingPrompts[0] : null; + const cue = normalizeText(speakingPrompt?.cue || '').toLowerCase(); + // A contextual prompt must test a pattern that actually occurs in its + // scenario. Otherwise a cultural keyword such as "respeto" can be paired + // with an invitation-declining situation. + const scenarioPattern = didactics.corePatterns + .map((entry) => corePatternTarget(entry)) + .find((candidate) => candidate && cue.includes(normalizeText(candidate).toLowerCase())); + const gapExercise = scenarioPattern + ? { + exerciseTypeId: 1, + title: `${lesson.title}: Kernmuster ergänzen`, + instruction: 'Fülle die Lücke mit dem passenden Bisaya-Ausdruck.', + questionData: { type: 'gap_fill', text: '{gap}', gaps: 1 }, + answerData: { type: 'gap_fill', answers: [scenarioPattern] }, + explanation: `Das Kernmuster lautet: "${scenarioPattern}".` + } + : buildGapExercise(lesson.title, pattern); if (!gapExercise) return null; return {