- New "New Words Per Session" setting (verbs/nouns/adjectives, 0–20 or
All, default 10). Session builders now fill with due reviews first, then
add fresh words only up to that throttle in the leftover room — so reviews
take priority and new vocab is introduced steadily. Fixes both flashcards
and multiple choice; Review Learned untouched.
- New per-type word-status metrics in Settings (New / Overdue / Due today /
Upcoming / Learned + Total), scoped to the enabled levels, shown under the
Verb Levels and Vocabulary Levels sections. Backed by WordStatusMetrics.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mirror the four-entry Vocabulary section for nouns and adjectives, so each
POS gets the same set of practice modes the verb flow already had:
- Noun/Adjective Flashcards (existing) — English → Spanish reveal with
article for nouns. Now accepts `kind:` to share the view with the
Review-Learned cram pass.
- Noun/Adjective Multiple Choice — English prompt, 4 Spanish options
drawn from the current session pool (1 correct + 3 random distractors).
Same SRS rating writes as Flashcards.
- Review Learned — `NounFlashcardPracticeView(kind: .reviewLearned)` and
the adjective equivalent. Cycles through already-studied lexemes with
no schedule changes; mirrors `VocabFlashcardPracticeView`'s
reviewLearned kind.
- Noun/Adjective Review — fetches due `LexemeReviewCard` rows by POS,
Spanish-front / English-reveal flashcards rated directly against the
SRS schedule. Each exposes a static `dueCount(context:)` used by the
practice-row badge.
Wiring:
- New `LexemeSessionKind` enum (standard / reviewLearned) in
LexemeSessionQueue.swift, mirroring `VocabSessionKind`.
- Noun + Adjective Flashcard views branch load/persist/answer on `kind`
so Review Learned doesn't touch the persisted study group or reschedule
cross-session SRS.
- Practice screen gets dedicated "Nouns" and "Adjectives" sections
(between Vocabulary and Reading), each with 4 NavigationLinks shaped
exactly like the Vocabulary section. The previous single-link Noun and
Adjective entries in the Reading section are removed.
- PracticeView caches `nounDueCount` / `adjectiveDueCount` in @State and
refreshes on appear + after sessions end, so the badge doesn't trigger
LexemeReviewCard fetchCount on every body re-evaluation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add SRS-driven noun and adjective flashcards modeled on the existing verb
flashcard flow:
- SharedModels/Lexeme — catalog of non-verb vocab, frequency-ranked, with
gender for nouns and optional example sentences. Seeded from a bundled
vocab_lexemes.json built by Scripts/vocab/build_lexemes.py, which joins
frequency.csv + es-en.data from a pinned doozan/spanish_data commit
(CC-BY-SA: hermitdave/FrequencyWords + Wiktionary). 1,449 nouns and 600
adjectives, each with Wiktionary-sourced gender and (where available)
an example sentence with English translation.
- LexemeReviewCard + LexemeReviewStore — cloud-synced SM-2 SRS, keyed by
partOfSpeech + lexemeId + drillMode so future drill modes can coexist.
- LexemeSessionQueue + LexemePool — parallel to VocabSessionQueue; fresh
cards sort by frequency rank.
- LexemeStudyGroup — cloud-synced resumable session per
(partOfSpeech, drillMode).
- NounFlashcardPracticeView + AdjectiveFlashcardPracticeView — same flow
as VocabFlashcardPracticeView: English prompt → tap to reveal Spanish
→ Again/Hard/Good/Easy. Nouns reveal with their article (la taza, el
problema) so gender is taught alongside meaning, not as a separate
quiz. Example sentence shown when present.
CEFR-style level toggles:
- LexemeLevel enum (A1/A2/B1/B2/C1+) derived from frequencyRank with
standard Spanish-frequency-dictionary cutoffs (250/500/1000/2000).
- UserProgress.selectedLexemeLevels — cloud-synced multi-select, defaults
to A1+A2 on first launch.
- SettingsView gains a "Vocabulary Levels" section with five toggles; the
existing "Levels" section is renamed "Verb Levels" for clarity.
- Due SRS cards always surface regardless of toggles. Disabling a level
only stops new cards from that band entering the pool.
PracticeView gets "Nouns" and "Adjectives" rows under "Books".
DataLoader: new lexemeDataVersion gate that re-seeds the Lexeme table
from vocab_lexemes.json independent of book seeding. project.yml lists
the new JSON resource and the existing book_olly-vol2.json (which the
previous build was silently excluding because xcodegen rewrote the
project from project.yml).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The session queue was in-memory only: leaving the flashcard screen or
killing the app rebuilt it from scratch. The set of verbs you were
working through wasn't sticky.
VocabStudyGroup (new cloud-synced @Model) — the active standard study
set: the in-session queue (un-graduated verbs + each card's
learning-step state) as a JSON blob, plus the learned count. One active
group at a time, keyed "active-standard". CloudKit-synced, so the same
group follows you from iPad to iPhone.
VocabStudyGroupStore — fetch-or-create / persist / clear. activeGroup()
returns the newest if duplicates exist (two devices racing before sync
settles); clear() removes all duplicates.
VocabSessionQueue — CardState is now String-backed; added
init(entries:learnedCount:) to resume a saved queue and snapshot() to
export it.
VocabFlashcardPracticeView (standard kind):
- On load, resume the active group if one exists; otherwise build a
fresh set and persist it immediately.
- Every answer writes the updated queue to the group.
- Finishing the set clears the group; the completion button is now
"Next Set" and builds a fresh group.
Review Learned (reviewLearned kind) is unaffected — it's an ad-hoc cram
and intentionally isn't persisted; its button stays "Study Again".
Registered VocabStudyGroup in the cloud container schema.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Vocab Flashcards / Multiple Choice were a flat linear walk through a
shuffled list — rating a card "Hard" or "Again" did nothing to bring it
back. Replaced with a real two-layer SRS, matching how Anki separates
in-session learning steps from the long-term schedule.
VocabSessionQueue (new) — the in-session layer. Position-based learning
steps:
- Again → card reappears 5–8 cards later (state: learning)
- Hard → reappears 7–10 cards later (state: learning)
- Good → first time: → review state, reappears 16–24 cards later
already in review: graduates, leaves the session
- Easy → graduates immediately
A card you keep failing keeps cycling until you mark it Good twice or
Easy once. answer() returns a ReviewQuality only on graduation — that's
the single rating handed to the long-term VerbReviewStore, so
intermediate Again/Hard presses no longer thrash the cross-session
SM-2 schedule.
VocabVerbPool.sessionVerbs (rewritten) — due-first ordering + a 20-card
session cap. Overdue verbs (per VerbReviewCard.dueDate) come first,
most-overdue leading; then never-reviewed verbs by frequency rank.
Not-yet-due verbs are intentionally skipped — that's the SRS schedule
doing its job. A single sitting is now bounded instead of a 100+ card
slog.
Study Again — the completion screen gets a "Study Again" button that
rebuilds the queue from the same verb set (re-shuffled), so you can run
the whole set again after finishing.
Progress display switched from "1 of 110" to "N learned · M to go",
which reflects the live queue as cards requeue and graduate.
Both vocab views now share VocabSessionQueue + VocabVerbPool; the queue
struct is pure value-type logic, easy to reason about and test.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Image Playground's .illustration style can't depict abstract verbs —
"dar" (to give) generated a Shiba Inu in a meadow, "saber" a cake.
Verbs are actions, not objects; a static illustration rarely helps and
the irrelevant output was pure noise.
Removed:
- VerbIllustration view (was in VocabFlashcardPracticeView).
- VocabImageService.swift entirely — VerbIllustration was its only
consumer.
- vocabImageService from the app environment in ConjugaApp.
- The illustration row from both vocab session reveal panes.
Vocab card reveal is now: Spanish infinitive + example sentence +
rating buttons. The example sentence already carries the learning
context.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Vocab Practice used a deck picker over VocabCard rows. That meant it
ignored the Settings level toggles entirely and operated on a totally
separate vocabulary universe than the conjugation modes. Rewired
end-to-end:
Pool source
Replaced VocabCard with the Verb table. The pool is now
ReferenceStore.fetchVerbs(selectedLevels: UserProgress.selectedVerbLevels)
— the same call PracticeSessionService uses for conjugation. Changes
to level toggles in Settings (or the Verbs tab, which also writes to
this field) immediately affect Vocab Practice.
Entry flow
Deleted VocabPracticeEntryView. Practice → Vocabulary now has two
direct entries:
• Vocab Flashcards — verb.english → tap → verb.infinitive
• Vocab Multiple Choice — verb.english → pick from 4 infinitives
Both pull from the same level-filtered pool, shuffled.
Per-verb SRS
New VerbReviewCard @Model (cloud-synced, mirrors CourseReviewCard's
SM-2 fields but keyed by verbId). VerbReviewStore.rate(verbId:quality:)
applies the existing SRSEngine. Registered in cloud container schema
in ConjugaApp.swift.
Example sentences
Lazy-generated via VerbExampleGenerator on first reveal, cached
through VerbExampleCache (same path VerbDetailView uses). Empty until
the example arrives — block hides itself if Apple Intelligence isn't
available.
AI illustration
VerbIllustration replaces VocabIllustration; same Image Playground
pipeline. Cache key uses ("verb", infinitive, english) so verbs and
course-deck vocab never collide.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Practice tab restructured into three sections:
- Conjugation: the 6 conjugation modes + the Common Tenses / Weak
Verbs / Irregularity Drills focus buttons.
- Vocabulary: new "Vocab Practice" entry (flashcard or multiple
choice, deck-picker on entry) + the existing Vocab Review.
- Reading: Stories, Books, Lyrics, Conversation, Listening, Cloze
(moved here from the flat list).
VocabPracticeEntryView lets the user pick any course/textbook deck
(or "All decks") and a mode (Flashcard / Multiple Choice). Last-used
choice is remembered via @AppStorage.
VocabFlashcardPracticeView:
Front shows the English meaning. Tap to reveal the Spanish word,
example sentences from the card, an AI-generated illustration of
the concept, and Again/Hard/Good/Easy rating buttons. SRS updates
via the existing CourseReviewStore.rate() path.
VocabMultipleChoicePracticeView:
English prompt, 4 Spanish options. Distractors come from the same
deck and prefer matching part-of-speech (via
DictionaryService.lookup) — falls back to random when POS is
unknown or the deck has fewer than 4 cards. After answer: reveal
correct/incorrect, the Spanish word, examples, illustration, and
the same rating buttons.
VocabImageService wraps Apple Intelligence's ImageCreator
(iOS 18.2+) for on-device illustration generation. Caches PNG
results to disk under Caches/VocabImages keyed by
SHA256(deck+ES+EN). In-flight dedup keeps concurrent requests for
the same key sharing one task. Falls back to a placeholder UI when
Apple Intelligence isn't available (older devices / disabled in
Settings) — detected lazily on the first failed ImageCreator init.
EN-first direction is enforced regardless of the underlying deck's
isReversed flag, so the user sees the English-to-Spanish recall
direction they asked for even when practising a reversed course
deck.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After the full enrichment pass, both the Tenses and Grammar surfaces of
the Guide tab cover overlapping material — WEIRDO appears in both
"Subjuntivo Presente" and "Subjunctive Triggers", preterite↔imperfect
contrast in three places, etc. Instead of trimming either body and
losing content, add a small chip row at the top of each detail view
linking directly across.
GuideCrossLinks.swift (new) — curated tense→[noteId] map covering 18 of
the 20 tenses. The two without aligned notes (ind_pluscuamperfecto,
ind_preterito_anterior) don't show chips. The reverse map (noteId→
[tenseId]) is derived once at static init and sorted by canonical tense
order so chips appear in conjugation-table order.
GuideDetailView — "Related grammar" indigo chip row directly under the
header. Tap a chip → switch to the Grammar segment with that note
selected.
GrammarNoteDetailView — "Used in tenses" orange chip row directly under
the title. Tap a chip → switch to the Tenses segment with that tense
selected.
The GuideView segment-change handler now only clears the *other* tab's
selection so programmatic jumps keep their destination intact; manual
segment swipes still feel "fresh" like before.
No content is removed. Users get a deeper-dive path one tap away in
either direction, and the redundancy becomes a feature instead of a
maintenance hazard.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a TTS read-along to the book reader. Tap the play button in the
toolbar; AVSpeechSynthesizer reads the chapter paragraph-by-paragraph
with the current word highlighted in yellow, auto-scrolling the active
paragraph to centre. Tap any word during read-along to pause and open
the definition sheet; reading resumes when the sheet dismisses.
Behavior per spec:
- Tap-to-define interrupts the synth (pauseSpeaking at: .immediate) and
resumes on sheet dismiss.
- Voice picker sheet (waveform.circle toolbar button) lists installed
Spanish voices grouped by Premium / Enhanced / Default quality, with
a "Download more voices…" row that opens iOS Settings (no public
deep-link to Accessibility → Spoken Content exists; the footer spells
out the path).
- Speed picker (Slow / Normal / Fast) drives AVSpeechUtterance.rate.
- Stops at chapter end, no auto-advance to the next chapter.
- Vocabulary lines shaped `palabra = meaning` are skipped — the synth
would otherwise say "palabra equals meaning" and they're reference
material, not prose.
Audio session uses .playback + .spokenAudio mode and is properly
deactivated with .notifyOthersOnDeactivation on stop() so music apps
resume cleanly after reading ends.
Voice/rate persisted via @AppStorage; controller picks them up
onAppear and writes back through Bindings the picker mutates.
Word-index space in BookSpeechController.wordRanges(in:) matches
BookReaderView's split(separator: " ") rendering exactly — both split
on ASCII U+0020 only, so willSpeakRange callbacks resolve to the right
visible word.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New "Books" row in the Practice tab opens a library of bundled bilingual
books. Each chapter renders Spanish paragraph-by-paragraph; tap any
word for a definition sheet (DictionaryService with on-device AI
fallback), or toggle the toolbar button to swap to the pre-computed
English translation inline.
Local-only Book + BookChapter SwiftData models added to the local
container schema (reset version bumped to 5). DataLoader.seedBooks
walks the bundle for `book_*.json` resources, so future books drop in
without touching app code — just bundle a new JSON and bump
bookDataVersion.
First book: Olly Richards' "Spanish Short Stories For Beginners
Vol 2" — 13 chapters, 2,646 paragraphs, bilingual.
Scripts/books/ is the repeatable pipeline for future EPUBs:
extract_epub.py → translate_chapters.py (per-chapter resumable jobs) →
bundle_book.py. Translation is done by parallel Claude Code subagents
reading per-job input files and writing output files — no API key
required, matching the pattern used for the textbook vocab vision
pass. See Scripts/books/README.md for the full how-to.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per-week "Extra Study (N)" row appears at the end of each LanGo week
section when at least one card is marked. Cards are marked from inside
VocabFlashcardView via a star next to the speaker on reveal. Marks are
keyed by the same SHA256 hash CourseReviewCard uses, so a mark and its
SRS state describe the same logical card.
ExtraStudyMark is CloudKit-synced (private DB), with uniqueness enforced
by fetch-or-create on id since CloudKit forbids @Attribute(.unique).
Skipped for textbook courses: DeckStudyView nils out the mark context
when the deck's courseName matches a TextbookChapter, and CourseView
hides the row when the active course is a textbook — so there are no
orphan marks the user can't reach.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- VideoActionsView: stack icon over title via a custom label style so
the Stream/Download/Play row fits iPhone width without wrapping
mid-word ("Stre am", "Down load").
- project.yml: add textbook_data.json and textbook_vocab.json to the
Conjuga target's Copy Bundle Resources. They were missing, so
DataLoader.seedTextbookData early-returned every launch and the
textbook never appeared in the Book tab. Regenerated Conjuga.xcodeproj.
Each of the 56 tense guides and grammar notes gets a curated YouTube video
attached (54 with picks, 2 silent nulls on rare / hard-to-find topics).
Users can stream in YouTube/Safari, download via YouTubeKit for offline
viewing, or play the local MP4 full-screen via AVPlayer.
YouTubeVideoStore loads the bundled youtube_videos.json at launch and serves
lookups by tense id or grammar note id. VideoDownloadService resolves the
best progressive MP4 stream off the main actor (YouTubeKit isn't Sendable),
writes to documents/videos/<videoId>.mp4, and records a DownloadedVideo row
in the local SwiftData container so the app knows what's on disk across
launches.
VideoActionsButtonRow is the unified UI for both detail views: three large
buttons — Stream (red, always enabled), Download (blue, disabled while in
flight and after completion, shows progress), Play (green, enabled only
when downloaded). Full-screen cover on tap. Settings gains a Downloaded
Videos list with swipe-delete, total-size summary, and a 500 MB warning.
Local store reset version bumped to 4 for the new DownloadedVideo schema.
Known fragility: YouTubeKit scrapes YouTube's private stream API and will
break when YouTube changes their internal format. Streaming keeps working
regardless.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bundles the 100 most common reflexive verbs from spanishwithdaniel.com as a
canonical list and wires it through the UI. Compound list entries (recibirse
/ graduarse, equivocarse / confundirse) are split. Trailing prepositions and
set-phrase completions are captured as usageHint (e.g. acordarse "de",
ponerse "de acuerdo").
ReflexiveVerbStore loads the JSON at launch and exposes lookups by base
infinitive, both via @Environment for SwiftUI and a static shared instance
for services. Verbs whose bare infinitive isn't in the list skip the UI
treatment silently.
VerbDetailView shows a new Reflexive section with the reflexive infinitive,
usage hint, and English meaning when there is a match. VerbListView gains a
"Reflexive verbs only" filter alongside the existing Level and Irregularity
filters. Settings adds the same flag so it also constrains the practice
pool; PracticeSessionService applies the reflexive filter in all six pick
paths.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tapping a verb now shows six example sentences beneath the conjugation table,
one per core tense (Present, Preterite, Imperfect, Future, Present Subjunctive,
Imperative). Each example renders the tense label, Spanish sentence, and
English translation in the DeckStudyView style.
Generation uses Foundation Models with a @Generable schema that pins each
response to the requested tenseId and forces tú/nosotros subjects for
imperatives. Results are cached as JSON in the Caches directory keyed by
verb id (DictionaryService pattern); cache misses regenerate on demand.
Devices without Apple Intelligence see an inline notice instead of the
loading state. No network dependency.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously the chapter reader showed vocab tables as a flat list of OCR
lines — because Vision reads columns top-to-bottom, the Spanish column
appeared as one block followed by the English column, making pairings
illegible.
Now every vocab table renders as a 2-column grid with Spanish on the
left and English on the right. Supporting changes:
- New ocr_all_vocab.swift: bounding-box OCR over all 931 vocab images,
cluster lines into rows by Y-coordinate, split rows by largest X-gap,
detect 2- / 3- / 4-column layouts automatically. ~2800 pairs extracted
this pass vs ~1100 from the old block-alternation heuristic.
- merge_pdf_into_book.py now prefers bounding-box pairs when present,
falls back to the heuristic, embeds the resulting pairs as
vocab_table.cards in book.json.
- DataLoader passes cards through to TextbookBlock on seed.
- TextbookChapterView renders cards via SwiftUI Grid (2 cols).
- fix_vocab.py quarantine rule relaxed — only mis-pairs where both
sides are clearly the same language are removed. "unknown" sides
stay (bbox pipeline already oriented them correctly).
Textbook card count jumps from 1044 → 3118 active pairs.
textbookDataVersion bumped to 9.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New features:
- Offline Dictionary: reverse index of 175K verb forms + 200 common
words, cached to disk, powers instant word lookups in Stories
- Vocab SRS Review: spaced repetition for course vocabulary cards
with due count badge and Again/Hard/Good/Easy rating
- Cloze Practice: fill-in-the-blank using SentenceQuizEngine with
distractor generation from vocabulary pool
- Grammar Exercises: interactive quizzes for 5 grammar topics
(ser/estar, por/para, preterite/imperfect, subjunctive, personal a)
with "Practice This" button on grammar note detail
- Listening Practice: listen-and-type + pronunciation check modes
using Speech framework with word-by-word match scoring
- Conversational Practice: AI chat partner via Foundation Models
with 10 scenario types, saved to cloud container
Other changes:
- Add Conversation model to SharedModels and cloud container
- Add Info.plist keys for speech recognition and microphone
- Skip speech auth on simulator to prevent crash
- Fix preparing data screen to only show during seed/migration
- Extract courseDataVersion to static property on DataLoader
- Add "How Features Work" reference page in Settings
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Generate one-paragraph Spanish stories on-device using Foundation Models,
matched to user's level and enabled tenses. Every word is tappable —
pre-annotated words show instantly, others get a quick on-device AI
lookup with caching. English translation hidden by default behind a
toggle. Comprehension quiz with 3 multiple-choice questions. Stories
saved to cloud container for sync and persistence across resets.
Closes#9
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Track how long users spend studying by timing foreground sessions.
StudyTimerService starts on app active, stops on background, and
accumulates seconds into DailyLog.studySeconds (CloudKit-synced).
Dashboard shows today/total study time with a 7-day bar chart.
Closes#1
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Checkpoint exams appear after each week in the course view, testing
all words from week 1 through the current week within the same course.
Users can choose 25, 50, or 100 questions with even distribution
across weeks. Results are tracked separately from weekly tests.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New EnglishConjugator in SharedModels constructs English translations
by combining the verb's infinitive with person pronouns and tense
auxiliaries (e.g., abatir conditional yo → "I would knock down").
Covers all 20 tense IDs, handles 60+ irregular English verbs,
multi-word verbs, 3rd person rules, gerund and participle formation.
VerbDetailView shows the English below each conjugated form, plus a
legend explaining red = irregular conjugation. 42 tests.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New feature in the Practice tab that lets users search for Spanish songs
by artist + title, fetch lyrics from LRCLIB (free, no API key), pull
album art from iTunes Search API, auto-translate to English via Apple's
on-device Translation framework, and save for offline reading.
Components:
- SavedSong SwiftData model (local container, no CloudKit sync)
- LyricsSearchService actor (LRCLIB + iTunes Search, concurrent)
- LyricsSearchView (artist/song fields, result list with album art)
- LyricsConfirmationView (lyrics preview, auto-translation, save)
- LyricsLibraryView (saved songs list, swipe to delete)
- LyricsReaderView (Spanish lines with English subtitles)
- Practice tab integration (Lyrics button with NavigationLink)
- localStoreResetVersion bumped to 3 for schema migration
Co-Authored-By: Claude Opus 4.6 (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>