The example sentences in VerbDetailView (and the new vocab practice
modes) frequently used the wrong verb — a "tener" set would show
"Él estaba leyendo un libro" (estar), "Nosotros vamos a viajar" (ir),
"Tú debes estudiar" (deber). The model drifted off the target verb
partway through generating the 6-example batch, and nothing checked
the output.
Two defenses:
Prompt grounding — VerbExampleGenerator.generate now takes a
formsByTense map (tenseId → conjugated forms, from the new
ReferenceStore.conjugatedForms). Each tense line in the prompt
lists the verb's exact conjugated forms and instructs the model to
use one of them. The model echoes a real form instead of recalling
(and mis-recalling) the conjugation.
Output validation — every generated sentence is checked against the
conjugation table via accent/case-folded whole-word matching. Any
sentence that doesn't contain a real conjugated form of the verb is
rejected. Failures trigger one regeneration pass; anything still
wrong is dropped rather than displayed. Better to show 4 correct
examples than 6 with 2 wrong.
Cache invalidation — VerbExampleCache now persists a versioned
wrapper (version 2). Pre-fix cached example sets — which may contain
wrong-verb sentences — fail the version check and are discarded, so
they regenerate cleanly under the new path.
Callers updated: VerbDetailView, VocabFlashcardPracticeView,
VocabMultipleChoicePracticeView all build formsByTense from
ReferenceStore and pass it through.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
UserProgress gains selectedLevelsBlob and enabledIrregularCategoriesBlob
(mirrors the existing tense-blob pattern). The multi-level setter keeps the
legacy selectedLevel String in sync with the highest-ranked selection, so
widget sync, AI scenarios, and achievement checks keep working unchanged.
Legacy single-level users are migrated on first read.
Settings replaces the level Picker with per-level toggles and adds an
Irregular Types section with three toggles. Practice pool is the literal
intersection: empty levels means zero results, empty irregular categories
means no irregularity constraint.
Pure filter logic lives in SharedModels (PracticeFilter, VerbLevel.highest)
and is covered by 20 Swift Testing cases. ReferenceStore delegates so the
intersection behavior is unit-tested without a ModelContainer.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Root cause: the widget was opening the shared local.store with a 2-entity
schema (VocabCard, CourseDeck), causing SwiftData to destructively migrate
the file and drop the 4 entities the widget didn't know about (Verb,
VerbForm, IrregularSpan, TenseGuide). The main app would then re-seed on
next launch, and the cycle repeated forever.
Fix: move Verb, VerbForm, IrregularSpan, TenseGuide from the app target
into SharedModels so both the main app and the widget use the exact same
types from the same module. Both now declare all 6 local entities in their
ModelContainer, producing identical schema hashes and eliminating the
destructive migration.
Other changes bundled in this commit (accumulated during debugging):
- Split ModelContainer into localContainer + cloudContainer (no more
CloudKit + non-CloudKit configs in one container)
- Add SharedStore.localStoreURL() helper and a global reference for
bypass-environment fetches
- One-time store reset mechanism to wipe stale schema metadata from
previous broken iterations
- Bootstrap/maintenance split so only seeding gates the UI; dedup and
cloud repair run in the background
- Sync status toast that shows "Syncing" while background maintenance
runs (network-aware, auto-dismisses)
- Background app refresh task to keep the widget word-of-day fresh
- Speaker icon on VerbDetailView for TTS
- Grammar notes navigation fix (nested NavigationStack was breaking
detail pane on iPhone)
- Word-of-day widget swaps front/back when the deck is reversed so the
Spanish word always shows in bold
- StoreInspector diagnostic helper for raw SQLite table inspection
- Add Conjuga scheme explicitly to project.yml so xcodegen doesn't drop it
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>