From 5fa1cc3921cdde57900ad33add5e262b1a852602 Mon Sep 17 00:00:00 2001 From: Trey t Date: Sat, 11 Apr 2026 22:22:28 -0500 Subject: [PATCH] Filter phonetic glosses from Complete the Sentence quiz MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Examples shorter than 4 words (like pronunciation guides "discutir(dees-koo-teer)") are now rejected by both isBlankResolvable and buildQuestion. The engine only picks real multi-word sentences for the quiz prompt. Every card already has at least one real sentence alongside its phonetic entries, so no data regeneration is needed — the filter alone fixes the issue. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../SharedModels/SentenceQuizEngine.swift | 6 +++ .../SentenceQuizEngineTests.swift | 46 +++++++++++++------ 2 files changed, 39 insertions(+), 13 deletions(-) diff --git a/Conjuga/SharedModels/Sources/SharedModels/SentenceQuizEngine.swift b/Conjuga/SharedModels/Sources/SharedModels/SentenceQuizEngine.swift index 8f185a7..bfe5ceb 100644 --- a/Conjuga/SharedModels/Sources/SharedModels/SentenceQuizEngine.swift +++ b/Conjuga/SharedModels/Sources/SharedModels/SentenceQuizEngine.swift @@ -60,6 +60,7 @@ public struct SentenceQuizEngine { public static func buildQuestion(for card: VocabCard, exampleIndex: Int) -> Question? { guard exampleIndex >= 0, exampleIndex < card.examplesES.count else { return nil } let sentence = card.examplesES[exampleIndex] + guard sentence.split(separator: " ").count >= minimumWordCount else { return nil } let sentenceEN = exampleIndex < card.examplesEN.count ? card.examplesEN[exampleIndex] : "" let storedBlank = exampleIndex < card.examplesBlanks.count ? card.examplesBlanks[exampleIndex] : "" @@ -79,8 +80,13 @@ public struct SentenceQuizEngine { // MARK: - Private + /// Minimum number of whitespace-separated tokens for an example to count as + /// a real sentence (filters out phonetic glosses like "discutir(dees-koo-teer)"). + public static let minimumWordCount = 4 + private static func isBlankResolvable(card: VocabCard, exampleIndex: Int) -> Bool { let sentence = card.examplesES[exampleIndex] + guard sentence.split(separator: " ").count >= minimumWordCount else { return false } let storedBlank = exampleIndex < card.examplesBlanks.count ? card.examplesBlanks[exampleIndex] : "" if !storedBlank.isEmpty, sentence.range(of: storedBlank, options: .caseInsensitive) != nil { return true diff --git a/Conjuga/SharedModels/Tests/SharedModelsTests/SentenceQuizEngineTests.swift b/Conjuga/SharedModels/Tests/SharedModelsTests/SentenceQuizEngineTests.swift index 8421eab..ebfa9d1 100644 --- a/Conjuga/SharedModels/Tests/SharedModelsTests/SentenceQuizEngineTests.swift +++ b/Conjuga/SharedModels/Tests/SharedModelsTests/SentenceQuizEngineTests.swift @@ -58,7 +58,7 @@ struct SentenceQuizEngineTests { examplesES: [ "Ella prepara la cena.", "Los niños van al parque.", - "Quiero comer ahora." + "Quiero comer algo ahora." ], examplesEN: ["", "", ""] ) @@ -66,6 +66,26 @@ struct SentenceQuizEngineTests { #expect(SentenceQuizEngine.resolvableIndices(for: card) == [2]) } + @Test("Phonetic glosses are rejected (too few words)") + func phoneticGlossRejected() { + let card = VocabCard( + front: "discutir", + back: "to discuss", + deckId: "d", + examplesES: [ + "discutir(dees-koo-teer)", + "INTRANSITIVE VERB", + "Los amigos van a discutir el tema." + ], + examplesEN: ["", "", "The friends are going to discuss the topic."] + ) + // Only index 2 is a real sentence (≥4 words AND contains the target) + #expect(SentenceQuizEngine.hasValidSentence(for: card)) + #expect(SentenceQuizEngine.resolvableIndices(for: card) == [2]) + // Phonetic entry at index 0 returns nil + #expect(SentenceQuizEngine.buildQuestion(for: card, exampleIndex: 0) == nil) + } + // MARK: - buildQuestion (deterministic) @Test("Builds question from substring match, preserves original casing") @@ -151,12 +171,12 @@ struct SentenceQuizEngineTests { front: "hola", back: "hello", deckId: "d", - examplesES: ["Hola, ¿cómo estás?"], - examplesEN: ["Hello, how are you?"] + examplesES: ["Hola amiga, ¿cómo estás hoy?"], + examplesEN: ["Hello friend, how are you today?"] ) let question = SentenceQuizEngine.buildQuestion(for: card, exampleIndex: 0) #expect(question?.blankWord == "Hola") - #expect(question?.displayTemplate == "_____, ¿cómo estás?") + #expect(question?.displayTemplate == "_____ amiga, ¿cómo estás hoy?") } @Test("Blanks phrase cards when target front contains spaces") @@ -208,14 +228,14 @@ struct SentenceQuizEngineTests { back: "to eat", deckId: "d", examplesES: [ - "Ella prepara la cena.", // unresolvable - "Los niños van al parque.", // unresolvable - "Quiero comer ahora.", // resolvable (substring) - "El perro come su comida." // unresolvable — note "come" is a substring but "comer" is not + "Ella prepara la cena.", // unresolvable (no "comer") + "Los niños van al parque.", // unresolvable + "Quiero comer algo rico ahora.", // resolvable (substring, ≥4 words) + "El perro come su comida diaria." // unresolvable — "come" but not "comer" ], examplesEN: ["", "", "", ""] ) - // Only index 2 is resolvable (contains "comer" literally) + // Only index 2 is resolvable (contains "comer" literally and has ≥4 words) for _ in 0..<25 { let q = SentenceQuizEngine.buildQuestion(for: card) #expect(q?.exampleIndex == 2) @@ -242,8 +262,8 @@ struct SentenceQuizEngineTests { front: "comer", back: "to eat", deckId: "d", - examplesES: ["Yo como.", "Tú comes."], - examplesEN: ["I eat.", "You eat."], + examplesES: ["Yo como mucho pan.", "Tú comes en casa."], + examplesEN: ["I eat a lot of bread.", "You eat at home."], examplesBlanks: ["como"] // only covers index 0 ) // Index 0: stored blank match @@ -260,8 +280,8 @@ struct SentenceQuizEngineTests { front: "perro", back: "dog", deckId: "d", - examplesES: ["El perro ladra."], - examplesEN: ["The dog barks."] + examplesES: ["El perro ladra todo el día."], + examplesEN: ["The dog barks all day."] ) let q = SentenceQuizEngine.buildQuestion(for: card, exampleIndex: 0) #expect(q?.displayTemplate.contains(SentenceQuizEngine.blankMarker) == true)