Fix Full Table eligibility — accept ordinary verbs, reject orto
The eligibility filter required every form's regularity tag to equal "regular", but the data uses four labels: - regular (179 forms — curated paradigm verbs) - ordinary (50,992 forms — pattern-following verbs like hablar, comer) - irregular (8,653) - orto (176 — orthographic spelling changes like busqué) Result was a 27-combo eligible pool, ~26 of which were -ir verbs in present tense — every Full Table prompt landed on the same handful of verbs. Pulled the rule into a SharedModels function (FullTableEligibility) so it's testable in isolation. Accepts "regular" + "ordinary" (both mean "follows the pattern"); rejects "irregular" and "orto". 9 unit tests cover the matrix including edge cases (incomplete forms, mixed labels, unknown values). PracticeSessionService.makePromptIfFullyRegular now delegates to FullTableEligibility, sorting forms by personIndex so the regularity array lines up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -140,8 +140,8 @@ struct PracticeSessionService {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a `FullTablePrompt` if this verb's forms in the given tense are
|
/// Returns a `FullTablePrompt` if this verb's forms in the given tense
|
||||||
/// all marked `regular` and complete. Nil otherwise.
|
/// follow the regular pattern (per `FullTableEligibility`). Nil otherwise.
|
||||||
private func makePromptIfFullyRegular(
|
private func makePromptIfFullyRegular(
|
||||||
verb: Verb,
|
verb: Verb,
|
||||||
tenseId: String,
|
tenseId: String,
|
||||||
@@ -149,8 +149,12 @@ struct PracticeSessionService {
|
|||||||
) -> FullTablePrompt? {
|
) -> FullTablePrompt? {
|
||||||
let forms = referenceStore.fetchForms(verbId: verb.id, tenseId: tenseId)
|
let forms = referenceStore.fetchForms(verbId: verb.id, tenseId: tenseId)
|
||||||
guard !forms.isEmpty else { return nil }
|
guard !forms.isEmpty else { return nil }
|
||||||
if forms.contains(where: { $0.regularity != "regular" }) { return nil }
|
// Forms must arrive in personIndex order so the regularity array lines
|
||||||
return FullTablePrompt(verb: verb, tenseInfo: tenseInfo, forms: forms)
|
// up. `fetchForms` already sorts them, but assert for safety.
|
||||||
|
let sorted = forms.sorted { $0.personIndex < $1.personIndex }
|
||||||
|
let regularities = sorted.map { $0.regularity }
|
||||||
|
guard FullTableEligibility.isFullyRegular(regularities: regularities) else { return nil }
|
||||||
|
return FullTablePrompt(verb: verb, tenseInfo: tenseInfo, forms: sorted)
|
||||||
}
|
}
|
||||||
|
|
||||||
func rate(verbId: Int, tenseId: String, personIndex: Int, quality: ReviewQuality) -> [Badge] {
|
func rate(verbId: Int, tenseId: String, personIndex: Int, quality: ReviewQuality) -> [Badge] {
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// Decides whether a (verb, tense) combo is eligible for the Full Table
|
||||||
|
/// practice mode. Pulled out of `PracticeSessionService` so the rule can be
|
||||||
|
/// unit tested in isolation.
|
||||||
|
public enum FullTableEligibility {
|
||||||
|
|
||||||
|
/// `VerbForm.regularity` values understood by the data set.
|
||||||
|
public enum Regularity: String, Sendable {
|
||||||
|
case regular // paradigm-teaching verbs (small curated set)
|
||||||
|
case ordinary // pattern-following verbs (the bulk of the dataset)
|
||||||
|
case irregular
|
||||||
|
case orto // orthographic spelling change (e.g. busqué)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true when the given (verb, tense) combo is a candidate for
|
||||||
|
/// Full Table — i.e. it follows the regular pattern with no irregularity
|
||||||
|
/// and no orthographic spelling change.
|
||||||
|
///
|
||||||
|
/// The conjugation must be present in all 6 person slots; missing forms
|
||||||
|
/// disqualify the combo.
|
||||||
|
///
|
||||||
|
/// Accepted: `regular` (paradigm-teaching verbs) and `ordinary`
|
||||||
|
/// (pattern-following verbs — `hablar`, `comer`, `vivir`, etc.).
|
||||||
|
/// Rejected: `irregular` and `orto` (orthographic spelling changes).
|
||||||
|
public static func isFullyRegular(regularities: [String]) -> Bool {
|
||||||
|
guard regularities.count == 6 else { return false }
|
||||||
|
let acceptable: Set<String> = ["regular", "ordinary"]
|
||||||
|
return regularities.allSatisfy { acceptable.contains($0) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import Testing
|
||||||
|
@testable import SharedModels
|
||||||
|
|
||||||
|
@Suite("FullTableEligibility")
|
||||||
|
struct FullTableEligibilityTests {
|
||||||
|
|
||||||
|
// MARK: - Should be eligible
|
||||||
|
|
||||||
|
@Test("all-ordinary verb (e.g. hablar present) is eligible")
|
||||||
|
func allOrdinary() {
|
||||||
|
// hablar present: hablo / hablas / habla / hablamos / habláis / hablan
|
||||||
|
let r = Array(repeating: "ordinary", count: 6)
|
||||||
|
#expect(FullTableEligibility.isFullyRegular(regularities: r))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("all-regular paradigm verb (e.g. vivir present) is eligible")
|
||||||
|
func allRegular() {
|
||||||
|
// vivir present is tagged "regular" in the curated subset
|
||||||
|
let r = Array(repeating: "regular", count: 6)
|
||||||
|
#expect(FullTableEligibility.isFullyRegular(regularities: r))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("mixed regular + ordinary across persons is eligible")
|
||||||
|
func mixedRegularOrdinary() {
|
||||||
|
// The data never actually mixes these, but it shouldn't matter if it
|
||||||
|
// did — both labels mean "follows the regular pattern".
|
||||||
|
let r = ["regular", "ordinary", "regular", "ordinary", "regular", "ordinary"]
|
||||||
|
#expect(FullTableEligibility.isFullyRegular(regularities: r))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Should NOT be eligible
|
||||||
|
|
||||||
|
@Test("any irregular form rejects the combo")
|
||||||
|
func anyIrregular() {
|
||||||
|
var r = Array(repeating: "ordinary", count: 6)
|
||||||
|
r[3] = "irregular"
|
||||||
|
#expect(!FullTableEligibility.isFullyRegular(regularities: r))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("orthographic spelling change rejects the combo (excluded by design)")
|
||||||
|
func anyOrto() {
|
||||||
|
// buscar preterite: yo "busqué" carries an orto tag for the c→qu shift.
|
||||||
|
// Per design choice, orto verbs are NOT eligible for Full Table.
|
||||||
|
var r = Array(repeating: "ordinary", count: 6)
|
||||||
|
r[0] = "orto"
|
||||||
|
#expect(!FullTableEligibility.isFullyRegular(regularities: r))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("all-irregular rejects")
|
||||||
|
func allIrregular() {
|
||||||
|
let r = Array(repeating: "irregular", count: 6)
|
||||||
|
#expect(!FullTableEligibility.isFullyRegular(regularities: r))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Edge cases
|
||||||
|
|
||||||
|
@Test("incomplete forms (fewer than 6 persons) are rejected")
|
||||||
|
func incompleteForms() {
|
||||||
|
let r = Array(repeating: "ordinary", count: 5)
|
||||||
|
#expect(!FullTableEligibility.isFullyRegular(regularities: r))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("empty input is rejected")
|
||||||
|
func empty() {
|
||||||
|
#expect(!FullTableEligibility.isFullyRegular(regularities: []))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("unknown regularity value is rejected (defensive default)")
|
||||||
|
func unknownValue() {
|
||||||
|
var r = Array(repeating: "ordinary", count: 6)
|
||||||
|
r[2] = "garbage_value"
|
||||||
|
#expect(!FullTableEligibility.isFullyRegular(regularities: r))
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user