From bb719b3c7506dcfe7469eebb3ea040b821fdf2d5 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Thu, 27 Aug 2026 12:25:43 +0200 Subject: [PATCH] fix(vocab): reject lexical hints paired with sentences --- ...0827000000-remove-invalid-karon-srs-card.cjs | 17 +++++++++++++++++ backend/services/vocabService.js | 9 ++++++++- .../socialnetwork/VocabPracticeDialog.vue | 7 ++++++- frontend/src/views/social/VocabLessonView.vue | 7 ++++++- 4 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 backend/migrations-active/20260827000000-remove-invalid-karon-srs-card.cjs diff --git a/backend/migrations-active/20260827000000-remove-invalid-karon-srs-card.cjs b/backend/migrations-active/20260827000000-remove-invalid-karon-srs-card.cjs new file mode 100644 index 0000000..f8f7e79 --- /dev/null +++ b/backend/migrations-active/20260827000000-remove-invalid-karon-srs-card.cjs @@ -0,0 +1,17 @@ +'use strict'; + +module.exports = { + async up(queryInterface) { + // A legacy gap-fill hint was persisted as a vocabulary answer. The pair + // "jetzt/heute" → "Karon naa ko diri" is not a valid translation card. + await queryInterface.sequelize.query(` + DELETE FROM community.vocab_srs_item + WHERE lower(trim(learning)) = 'jetzt/heute' + AND lower(trim(reference)) = 'karon naa ko diri'; + `); + }, + + async down() { + // Do not restore an invalid study card. + } +}; diff --git a/backend/services/vocabService.js b/backend/services/vocabService.js index d02a507..d60a566 100755 --- a/backend/services/vocabService.js +++ b/backend/services/vocabService.js @@ -201,8 +201,15 @@ export default class VocabService { const rightLooksSentence = rightWords >= 3 || /[?!.].+\S/.test(rightText); const leftLooksShortFragment = leftWords <= 1 && leftText.length <= 12; const rightLooksShortFragment = rightWords <= 1 && rightText.length <= 12; + // A slash-separated hint such as "jetzt/heute" is a lexical alternative, + // never the translation of a full sentence such as "Karon naa ko diri". + const leftLooksLexicalHint = /\//.test(leftText) && leftWords <= 2 && leftText.length <= 24; + const rightLooksLexicalHint = /\//.test(rightText) && rightWords <= 2 && rightText.length <= 24; - return (leftLooksShortFragment && rightLooksSentence) || (rightLooksShortFragment && leftLooksSentence); + return (leftLooksShortFragment && rightLooksSentence) + || (rightLooksShortFragment && leftLooksSentence) + || (leftLooksLexicalHint && rightLooksSentence) + || (rightLooksLexicalHint && leftLooksSentence); } _calculateSrsSchedule(item, { correct } = {}) { diff --git a/frontend/src/dialogues/socialnetwork/VocabPracticeDialog.vue b/frontend/src/dialogues/socialnetwork/VocabPracticeDialog.vue index 5938648..688c868 100755 --- a/frontend/src/dialogues/socialnetwork/VocabPracticeDialog.vue +++ b/frontend/src/dialogues/socialnetwork/VocabPracticeDialog.vue @@ -739,8 +739,13 @@ export default { const rightLooksSentence = rightWords >= 3 || /[?!.].+\S/.test(rightText); const leftLooksShortFragment = leftWords <= 1 && leftText.length <= 12; const rightLooksShortFragment = rightWords <= 1 && rightText.length <= 12; + const leftLooksLexicalHint = /\//.test(leftText) && leftWords <= 2 && leftText.length <= 24; + const rightLooksLexicalHint = /\//.test(rightText) && rightWords <= 2 && rightText.length <= 24; - return (leftLooksShortFragment && rightLooksSentence) || (rightLooksShortFragment && leftLooksSentence); + return (leftLooksShortFragment && rightLooksSentence) + || (rightLooksShortFragment && leftLooksSentence) + || (leftLooksLexicalHint && rightLooksSentence) + || (rightLooksLexicalHint && leftLooksSentence); }, splitPhraseAlternatives(value) { const text = String(value || '').trim(); diff --git a/frontend/src/views/social/VocabLessonView.vue b/frontend/src/views/social/VocabLessonView.vue index c6c2ee5..2a9f3e7 100755 --- a/frontend/src/views/social/VocabLessonView.vue +++ b/frontend/src/views/social/VocabLessonView.vue @@ -2320,8 +2320,13 @@ export default { const rightLooksSentence = rightWords >= 3 || /[?!.].+\S/.test(rightText); const leftLooksShortFragment = leftWords <= 1 && leftText.length <= 12; const rightLooksShortFragment = rightWords <= 1 && rightText.length <= 12; + const leftLooksLexicalHint = /\//.test(leftText) && leftWords <= 2 && leftText.length <= 24; + const rightLooksLexicalHint = /\//.test(rightText) && rightWords <= 2 && rightText.length <= 24; - return (leftLooksShortFragment && rightLooksSentence) || (rightLooksShortFragment && leftLooksSentence); + return (leftLooksShortFragment && rightLooksSentence) + || (rightLooksShortFragment && leftLooksSentence) + || (leftLooksLexicalHint && rightLooksSentence) + || (rightLooksLexicalHint && leftLooksSentence); }, isTrainableLessonVocabPair(learning, reference) { const l = String(learning || '').trim();