Filter phonetic glosses from Complete the Sentence quiz

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) <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-04-11 22:22:28 -05:00
parent a51d2abd47
commit 5fa1cc3921
2 changed files with 39 additions and 13 deletions

View File

@@ -60,6 +60,7 @@ public struct SentenceQuizEngine {
public static func buildQuestion(for card: VocabCard, exampleIndex: Int) -> Question? { public static func buildQuestion(for card: VocabCard, exampleIndex: Int) -> Question? {
guard exampleIndex >= 0, exampleIndex < card.examplesES.count else { return nil } guard exampleIndex >= 0, exampleIndex < card.examplesES.count else { return nil }
let sentence = card.examplesES[exampleIndex] let sentence = card.examplesES[exampleIndex]
guard sentence.split(separator: " ").count >= minimumWordCount else { return nil }
let sentenceEN = exampleIndex < card.examplesEN.count ? card.examplesEN[exampleIndex] : "" let sentenceEN = exampleIndex < card.examplesEN.count ? card.examplesEN[exampleIndex] : ""
let storedBlank = exampleIndex < card.examplesBlanks.count ? card.examplesBlanks[exampleIndex] : "" let storedBlank = exampleIndex < card.examplesBlanks.count ? card.examplesBlanks[exampleIndex] : ""
@@ -79,8 +80,13 @@ public struct SentenceQuizEngine {
// MARK: - Private // 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 { private static func isBlankResolvable(card: VocabCard, exampleIndex: Int) -> Bool {
let sentence = card.examplesES[exampleIndex] let sentence = card.examplesES[exampleIndex]
guard sentence.split(separator: " ").count >= minimumWordCount else { return false }
let storedBlank = exampleIndex < card.examplesBlanks.count ? card.examplesBlanks[exampleIndex] : "" let storedBlank = exampleIndex < card.examplesBlanks.count ? card.examplesBlanks[exampleIndex] : ""
if !storedBlank.isEmpty, sentence.range(of: storedBlank, options: .caseInsensitive) != nil { if !storedBlank.isEmpty, sentence.range(of: storedBlank, options: .caseInsensitive) != nil {
return true return true

View File

@@ -58,7 +58,7 @@ struct SentenceQuizEngineTests {
examplesES: [ examplesES: [
"Ella prepara la cena.", "Ella prepara la cena.",
"Los niños van al parque.", "Los niños van al parque.",
"Quiero comer ahora." "Quiero comer algo ahora."
], ],
examplesEN: ["", "", ""] examplesEN: ["", "", ""]
) )
@@ -66,6 +66,26 @@ struct SentenceQuizEngineTests {
#expect(SentenceQuizEngine.resolvableIndices(for: card) == [2]) #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) // MARK: - buildQuestion (deterministic)
@Test("Builds question from substring match, preserves original casing") @Test("Builds question from substring match, preserves original casing")
@@ -151,12 +171,12 @@ struct SentenceQuizEngineTests {
front: "hola", front: "hola",
back: "hello", back: "hello",
deckId: "d", deckId: "d",
examplesES: ["Hola, ¿cómo estás?"], examplesES: ["Hola amiga, ¿cómo estás hoy?"],
examplesEN: ["Hello, how are you?"] examplesEN: ["Hello friend, how are you today?"]
) )
let question = SentenceQuizEngine.buildQuestion(for: card, exampleIndex: 0) let question = SentenceQuizEngine.buildQuestion(for: card, exampleIndex: 0)
#expect(question?.blankWord == "Hola") #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") @Test("Blanks phrase cards when target front contains spaces")
@@ -208,14 +228,14 @@ struct SentenceQuizEngineTests {
back: "to eat", back: "to eat",
deckId: "d", deckId: "d",
examplesES: [ examplesES: [
"Ella prepara la cena.", // unresolvable "Ella prepara la cena.", // unresolvable (no "comer")
"Los niños van al parque.", // unresolvable "Los niños van al parque.", // unresolvable
"Quiero comer ahora.", // resolvable (substring) "Quiero comer algo rico ahora.", // resolvable (substring, 4 words)
"El perro come su comida." // unresolvable note "come" is a substring but "comer" is not "El perro come su comida diaria." // unresolvable "come" but not "comer"
], ],
examplesEN: ["", "", "", ""] 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 { for _ in 0..<25 {
let q = SentenceQuizEngine.buildQuestion(for: card) let q = SentenceQuizEngine.buildQuestion(for: card)
#expect(q?.exampleIndex == 2) #expect(q?.exampleIndex == 2)
@@ -242,8 +262,8 @@ struct SentenceQuizEngineTests {
front: "comer", front: "comer",
back: "to eat", back: "to eat",
deckId: "d", deckId: "d",
examplesES: ["Yo como.", "Tú comes."], examplesES: ["Yo como mucho pan.", "Tú comes en casa."],
examplesEN: ["I eat.", "You eat."], examplesEN: ["I eat a lot of bread.", "You eat at home."],
examplesBlanks: ["como"] // only covers index 0 examplesBlanks: ["como"] // only covers index 0
) )
// Index 0: stored blank match // Index 0: stored blank match
@@ -260,8 +280,8 @@ struct SentenceQuizEngineTests {
front: "perro", front: "perro",
back: "dog", back: "dog",
deckId: "d", deckId: "d",
examplesES: ["El perro ladra."], examplesES: ["El perro ladra todo el día."],
examplesEN: ["The dog barks."] examplesEN: ["The dog barks all day."]
) )
let q = SentenceQuizEngine.buildQuestion(for: card, exampleIndex: 0) let q = SentenceQuizEngine.buildQuestion(for: card, exampleIndex: 0)
#expect(q?.displayTemplate.contains(SentenceQuizEngine.blankMarker) == true) #expect(q?.displayTemplate.contains(SentenceQuizEngine.blankMarker) == true)