feat(bisaya-course): enhance gap-fill exercise processing and validation
All checks were successful
Deploy to production / deploy (push) Successful in 2m54s

- Added `buildCorePatternGlossLookup` to create a mapping of core patterns for improved hint sanitization.
- Implemented `sanitizeGapFillHintText` to validate and replace hints based on answer word count and gloss availability, generating fixes and warnings as needed.
- Updated `sanitizeExerciseForConsistency` to incorporate new sanitization logic for gap-fill exercises, ensuring consistent exercise data handling.
- Enhanced logging in `createBisayaCourseContent` to provide detailed feedback on exercise processing and validation outcomes.
This commit is contained in:
Torsten Schulz (local)
2026-04-14 15:33:00 +02:00
parent 78da376c5b
commit f841a35501

View File

@@ -74,6 +74,84 @@ function collectExerciseAuditWarnings(lessonTitle, exerciseData, exerciseNumber)
return warnings;
}
function buildCorePatternGlossLookup(didactics) {
const map = new Map();
const patterns = Array.isArray(didactics?.corePatterns) ? didactics.corePatterns : [];
patterns.forEach((entry) => {
const normalized = normalizeCorePatternEntry(entry);
if (!normalized?.target || !normalized?.gloss) return;
map.set(normalized.target.toLowerCase(), normalized.gloss);
});
return map;
}
function sanitizeGapFillHintText(lessonTitle, text, answers, glossLookup) {
const source = String(text || '');
const normalizedAnswers = Array.isArray(answers)
? answers.map((answer) => normalizeText(answer))
: [];
if (!source || normalizedAnswers.length === 0) {
return { text: source, fixes: [], warnings: [] };
}
const hintRegex = /\(([^)]+)\)/g;
let hintIndex = 0;
const fixes = [];
const warnings = [];
const rebuilt = source.replace(hintRegex, (full, inner) => {
const answer = normalizedAnswers[hintIndex] || '';
const hint = normalizeText(inner);
hintIndex += 1;
if (!answer || !hint) return full;
const answerWords = countWords(answer);
const hintWords = countWords(hint);
if (!(answerWords <= 2 && hintWords >= 4)) {
return full;
}
const mappedGloss = normalizeText(glossLookup.get(answer.toLowerCase()) || '');
if (mappedGloss && countWords(mappedGloss) <= 3) {
fixes.push(`[${lessonTitle}] "${answer}": "${hint}" -> "${mappedGloss}"`);
return `(${mappedGloss})`;
}
warnings.push(
`[${lessonTitle}] Keine sichere Gloss für "${answer}" gefunden; langer Hinweis bleibt: "${hint}"`
);
return full;
});
return { text: rebuilt, fixes, warnings };
}
function sanitizeExerciseForConsistency(lesson, exerciseData, didactics) {
const exercise = { ...exerciseData };
const questionData = { ...(exerciseData?.questionData || {}) };
const answerData = { ...(exerciseData?.answerData || {}) };
const fixes = [];
const warnings = [];
if (questionData.type === 'gap_fill') {
const glossLookup = buildCorePatternGlossLookup(didactics);
const sanitized = sanitizeGapFillHintText(
lesson.title,
questionData.text,
Array.isArray(answerData.answers) ? answerData.answers : [],
glossLookup
);
if (sanitized.text !== questionData.text) {
questionData.text = sanitized.text;
fixes.push(...sanitized.fixes);
}
warnings.push(...sanitized.warnings);
}
exercise.questionData = questionData;
exercise.answerData = answerData;
return { exercise, fixes, warnings };
}
function normalizeCorePatternEntry(entry) {
if (entry === null || entry === undefined || entry === '') {
return null;
@@ -4292,6 +4370,7 @@ function getExercisesForLesson(lesson) {
async function createBisayaCourseContent() {
await sequelize.authenticate();
console.log('Datenbankverbindung erfolgreich hergestellt.\n');
const forceRebuildAll = process.env.VOCAB_FORCE_REBUILD_ALL === '1';
const systemUser = await findOrCreateSystemUser();
console.log(`Verwende System-Benutzer: ${systemUser.username} (ID: ${systemUser.id})\n`);
@@ -4362,33 +4441,44 @@ async function createBisayaCourseContent() {
where: { lessonId: lesson.id }
});
if (existingCount > 0 && !replacePlaceholders) {
if (existingCount > 0 && !replacePlaceholders && !forceRebuildAll) {
console.log(` ⏭️ Lektion ${lesson.lessonNumber}: "${lesson.title}" - bereits ${existingCount} Übung(en) vorhanden`);
continue;
}
if (replacePlaceholders && existingCount > 0) {
if ((replacePlaceholders || forceRebuildAll) && existingCount > 0) {
const deleted = await VocabGrammarExercise.destroy({ where: { lessonId: lesson.id } });
console.log(` 🗑️ Lektion ${lesson.lessonNumber}: "${lesson.title}" - ${deleted} Platzhalter entfernt`);
const reason = forceRebuildAll ? 'vollständig neu aufgebaut' : 'Platzhalter entfernt';
console.log(` 🗑️ Lektion ${lesson.lessonNumber}: "${lesson.title}" - ${deleted} Übungen entfernt (${reason})`);
}
// Erstelle Übungen
const lessonDidactics = getLessonDidactics(lesson);
let exerciseNumber = 1;
for (const exerciseData of exercises) {
const { exercise, fixes, warnings } = sanitizeExerciseForConsistency(
lesson,
exerciseData,
lessonDidactics
);
if (process.env.VOCAB_STRICT_AUDIT === '1') {
const warnings = collectExerciseAuditWarnings(lesson.title, exerciseData, exerciseNumber);
fixes.forEach((fix) => console.log(` 🛠️ ${fix}`));
warnings.forEach((warning) => console.warn(` ⚠️ ${warning}`));
}
const exerciseTypeId = await resolveExerciseTypeId(exerciseData);
if (process.env.VOCAB_STRICT_AUDIT === '1') {
const warnings = collectExerciseAuditWarnings(lesson.title, exercise, exerciseNumber);
warnings.forEach((warning) => console.warn(` ⚠️ ${warning}`));
}
const exerciseTypeId = await resolveExerciseTypeId(exercise);
await VocabGrammarExercise.create({
lessonId: lesson.id,
exerciseTypeId,
exerciseNumber: exerciseNumber++,
title: exerciseData.title,
instruction: exerciseData.instruction,
questionData: JSON.stringify(exerciseData.questionData),
answerData: JSON.stringify(exerciseData.answerData),
explanation: exerciseData.explanation,
title: exercise.title,
instruction: exercise.instruction,
questionData: JSON.stringify(exercise.questionData),
answerData: JSON.stringify(exercise.answerData),
explanation: exercise.explanation,
createdByUserId: course.ownerUserId || systemUser.id
});
totalExercisesAdded++;