Changelog
All notable changes to this project will be documented in this file.
[0.8.2.1] - 2026-05-28 — "Show context" gains a keyboard shortcut
Added
- Press
Cto reveal context during review. The "Show context" button on vocab flashcards now responds to theCkey, so you can pull up the source sentence without lifting your hands off the keyboard between Space (reveal answer) and 1–4 (rate). The keybind only fires on vocab cards that have a source sentence attached, ignoresCmd/Ctrl+Cso copying the front text still works, and records the reveal in the hint metric exactly like a click does.
[0.8.2.0] - 2026-05-27 — Vocab cards remember where you met the word
Added
- Vocab flashcards now carry their source sentence. When you tap a word in the Reader and save it, Haechi keeps the exact sentence you read it in — not just the dictionary gloss. During review, a quiet "Show context" button sits on the front of every vocab card; tap it before you flip and the original sentence appears with your word highlighted, captioned with the passage it came from. Words learned in context stick far better than words drilled in isolation, and the Reader was already throwing that context away.
- Your existing vocab deck gets context retroactively. A one-time backfill scans your reading history and attaches the best matching sentence to every vocab card you'd already captured — including
~하다/~되다verbs and other words Korean splits across multiple morphemes. Cards it can't match stay plain (and get picked up later if you read a passage that uses the word). - Reveal usage is tracked so you can tell if it's working. Each review records whether you needed the context hint, feeding a true-lapse-rate measurement (
scripts/measure-vocab-lapse-rate.ts) you can run before and 30 days after to see whether remembered-without-help actually improved. Baseline at ship: a 47% true-lapse rate on review-state vocab.
Changed
- First review session after this update briefly syncs your cards. A one-time "Syncing your cards…" moment makes the new context show up immediately instead of appearing only on your second visit.
Fixed
POST /api/reader/capture,/api/review, and/api/syncvalidate and round-trip the new provenance and hint fields with full server-side defense (passage ownership, sentence-boundary checks, vocab-only hint filtering) so the context shown during review always comes from your own reading, online or offline.
[0.8.1.0] - 2026-05-19 — Speak: in-app shadowing (Reader mic + scoring)
Added
- Shadowing is now live inside Haechi. Open any Reader passage and a mic button sits at the end of every sentence. Tap it: Haechi speaks the sentence in natural Korean (ElevenLabs), you record yourself, and you get a 0–100% pronunciation score against what you said (OpenAI Whisper + character-level Levenshtein). v0.8.0.0 shipped the engine; this release wires it into the app you actually use.
POST /api/speak/ttsandPOST /api/speak/score— Astro proxy endpoints that gate the speak service behind your better-auth session plus aSPEAK_ALLOWED_USER_IDSallowlist (v0 soft-launch), forwarding the sharedSPEAK_API_KEYserver-to-server over Railway private networking.- Recording length is generous in the Reader. Korean news sentences run long, so the Reader gives you up to 120 seconds per take (the 8-second cap stays the default elsewhere). Press stop whenever you're done.
- "Resize text" hint in the Reader. A quiet caption points at browser zoom (⌘/Ctrl and +/−) for anyone who didn't know they could make the Korean bigger — no custom control, by design.
- Scores are saved per card in a new
productionScorecolumn so future versions can grow per-example history. For now it is practice only (a note in the result says so) and it does not change your review schedule.
Fixed
- The speak service is reachable in production: it now binds IPv6 (
[::]) so Railway's IPv6-only private network can route to it, and the Astro CSRF origin check no longer rejects the multipart upload behind Railway's TLS-terminating edge. - A failed score upload retries once automatically and keeps your recording in memory, so a flaky connection no longer forces you to record again.
- iOS Safari no longer freezes the widget when
MediaRecordercan't start — it shows a clear error and releases the microphone.
For contributors
- New Astro proxy endpoints (
src/pages/api/speak/{tts,score}.ts), theSpeakWidgetPreact island, Reader integration, and theproductionScoreschema column + migration0004. 27 new unit tests intests/api/speak/; the speak crate is up to 40 Rust tests. The server-latency readout is hidden behind alocalStoragespeak:debugflag (shown automatically in local dev). security.checkOriginis disabled inastro.config.mjs: it false-positives every form POST behind Railway's TLS-terminating edge, and Haechi's only form-content-type POST is/api/speak/score(every other mutating route is JSON, which the check never inspected). Rationale is in the config comment and DESIGN.md.
[0.8.0.0] - 2026-05-13 — Speak: Korean shadowing service + CLI (v0)
Added
speakRust workspace atspeak/— a new internal HTTP service plus a developer CLI for the shadowing loop. Goal per the design doc: turn Haechi from a reading/writing app into a reading/writing/speaking app, on YOUR own grammar + reader content. v0 ships the service and CLI; Astro-side mic integration in Reader/Writer is the next step.POST /ttsproxies ElevenLabs (eleven_multilingual_v2by default) and returnsaudio/mpeg. Capped at 500 characters per request so a compromised upstream caller can't translate into unbounded ElevenLabs spend.POST /scoreaccepts a multipart upload (audio+expected_text), forwards the audio to OpenAI Whisper withlanguage=kohinted, normalizes both transcript and expected text (trim → NFC → strip ASCII + CJK + smart-quote punctuation), and returns a character-level Levenshtein score in[0, 1]along with the raw transcript and anullshort-circuit when the audio is empty or silent. Expected text capped at 1000 characters; body limit raised to 32 MiB to fit uncompressed multi-channel PCM uploads.speak shadowCLI subcommand drives the end-to-end loop: fetches TTS, plays it via rodio (Symphonia mp3 decode), captures from the default mic via cpal with SPACE-to-start / SPACE-to-stop control and an 8-second hard cap, downmixes to mono and resamples to 16 kHz client-side (Whisper's internal working rate), writes a~/.speak/recordings/, submits to.wav /score, and prints an ANSI-colored result block (green ≥ 0.9, yellow ≥ 0.7, red below, dim for null).speak serveCLI subcommand runs the HTTP server. Authenticates the Astro caller viaSPEAK_API_KEY(constant-time compare viasubtle::ConstantTimeEq, case-insensitiveBearerscheme per RFC 6750).tts-qaprovider QA harness atspeak/src/bin/tts-qa.rs— generates paired OpenAI and ElevenLabs mp3 files from real Haechi grammar example sentences so you can listen side-by-side and pick a provider. Used to lock in ElevenLabs for the v0 service.rustfmt.tomlat the repo root plus aspeak/-scoped override (tab_spaces = 8) so the Rust code has a consistent format that ignores Astro/TS tooling.
Internal
- 37 unit tests in the
speakcrate. Covers NFC + punctuation normalization (Korean fullwidth + smart quotes), char-level Levenshtein on Hangul, mono-downmix and linear-interpolation resample, ANSI score-color thresholds, WAV round-trip via hound, ServeError → HTTP status mapping,tts-qafrontmatter + truncate helpers. - Error reporting: upstream provider error bodies (ElevenLabs / Whisper) are logged via
tracing::warn!server-side and redacted toprovider returned {status}for the caller. Multipart parse errors are similarly logged and reduced tomalformed multipart. The CLI'sCtrl+CandEsckeypresses are caught while the terminal is in raw mode soRawGuard::Dropruns and the user's shell isn't left without echo / line buffering.
[0.7.11.0] - 2026-05-13 — Grammar content audit (PR2): tighter definitions, no Korean leakage on recall cards
Changed
- Recall-card definitions now read as clean dictionary glosses. 43 of 62 grammar entries had a
definitionfield that either echoed something thetldralready said (definition/tldr redundancy) or, in 2 cases, embedded an illustrative Korean phrase paired with its English meaning — which broke FSRS English-side recall cards by leaking the form the user was supposed to produce. Both defects swept across the corpus. ~(으)ㄹ 때:학생일 때 'when I was a student'example phrase stripped from the definition.~도록:밤새도록example stripped. Both phrases are still in the entry body where they belong.- 41 entries had their
definitionshortened or rephrased to stop duplicating thetldr's structural depth. The pattern: when both fields said similar things, thedefinitionbecame a tight dictionary gloss and thetldrkept the morphology / dichotomies / sibling-pattern contrasts.
Internal
- 19 entries scanned and left clean (
지만,ㄹ뻔하다,ㄴ적이있다,ㄴ김에, and 15 others) — already had non-redundant definition/tldr pairs from earlier flagship audits. - 6 atomic commits on
feat/content-audit-pr2, batched alphabetically. Body prose, examples, vocab, andtldrfields untouched in all 43 files. All 260 vitest unit tests pass;bun run validate:contentclean (62 entries, 0 collisions). - Closes the three-dimension content QA audit captured in TODOS.md. Dimension 1 (Korean leakage in definitions) and dimension 2 (definition/tldr redundancy) shipped in this PR; dimension 3 (lazy Y&B attribution) shipped in v0.7.10.0.
[0.7.10.0] - 2026-05-13 — Grammar content audit: lazy Y&B attribution stripped from 60 entries
Changed
- Grammar entries now read like a reference, not a textbook summary. 60 of 62 entries had inline "Yeon & Brown classify…", "Y&B note…", "Per Y&B's framework…" framings that made the prose feel borrowed rather than authored. Stripped across all entries. Scholar citations (Kim 2010, King & Yeon 2002, Sohn 1992, Lukoff & Nam 1982), section references (§4.4.3.2, §7.5.4), and the canonical
source:frontmatter line all preserved — those carry actual information. - Two entries (
~(으)ㄴ/는 데다(가),~(으)ㄴ 셈이다) keep an explicit "the Yeon & Brown grammar doesn't cover this directly — sourced from NIKL" note. This is informative absence, not lazy attribution: it justifies why those entries cite NIKL's *한국어기초사전* instead of Y&B as their primary source.
Internal
- Audit dimension covered: lazy textbook attribution in body prose AND
tldrfrontmatter. The other two dimensions from the audit plan (Korean leakage indefinitionfields,definition/tldrredundancy) ship in a follow-up editorial pass. - 7 atomic commits on
feat/content-audit, batched by Y&B-hit density (high → low). All 260 vitest unit tests pass;bun run validate:contentclean (62 entries, 0 collisions).
[0.7.9.0] - 2026-05-12 — Dark mode audit + screenshot tooling + perf measurement
Fixed
- Grammar list card hover state previously used hardcoded
rgba(0,0,0,0.12)border +rgba(0,0,0,0.04)shadow that disappeared on dark mode. Replaced with a.grammar-card:hoverrule keyed offvar(--muted-text)(border) andcolor-mix(in srgb, var(--foreground) 8%, transparent)(shadow) so both adapt cleanly. - Reader popover shadow was a hardcoded
rgba(0, 0, 0, 0.12)invisible against dark backgrounds. Now derives from--foregroundso it lands as a soft dark shadow on light bg and a soft glow on dark bg.
Added
- Consolidated screenshot tool at
mockups/scripts/screenshots.mjs. Replaces four ad-hoc scripts (pr3-screenshots.mjs,body-font-compare-screenshot.mjs,long-title-screenshot.mjs,multi-page-screenshots.mjs). Supports presets (flagships,hero,public,compare,sweep), per-mode (--modes light,dark), per-viewport (--viewports 1280,375), and full-page vs viewport (--fullPage false). Forcesdata-themepost-navigation so dark-mode captures actually render dark. - Cold-load perf audit script at
mockups/scripts/perf-cold-load.mjs. Captures every resource on a fresh visit and summarizes by type. Used to verify the dual-serif typography didn't break the 220KB Latin cold-load budget.
Internal
- Cold-load on production grammar entry (
/grammar/지만): ~1.17 MB total, dominated by ~951 KB of fonts (Korean NSK subsets + Latin editorial serifs). JS ships at just 41 KB. Latin editorial fonts (Fraunces upright + italic, Newsreader upright + italic, Geist Mono) total ~231 KB — well inside the 220KB-ish target documented in DESIGN.md. - Cold-load on production index (
/): ~966 KB total (602 KB fonts, 167 KB JS). Smaller because Korean content is shorter (page heading only). - Dark mode visual audit across
/,/grammar/지만,/grammar/아어야되다,/changelog,/404. Two hardcoded-color bugs caught and fixed (above). Everything else adapts cleanly via the existing--foreground/--background/--muted-text/--surface/--accenttoken system. - Moved the four ad-hoc screenshot scripts out of
scripts/(project utility scripts) intomockups/scripts/(design-review artifacts) and consolidated into the single parameterized tool.
[0.7.8.2] - 2026-05-11 — Review card front + back now route through Newsreader
Fixed
- Review card faces (
.review-front,.review-back) now usevar(--font-serif-body)(Newsreader → Noto Serif KR fallback chain) instead of hardcoded NSK + Pretendard. Per-character fallback handles both card directions automatically: English content (recall prompts, vocab backs, definition meanings) renders in Newsreader; Hangul falls through to NSK. Earlier 0.7.8.1 review pass missed these because both elements had inlinefont-uiTailwind classes overriding the CSS. Tweaked review-front weight from 700 → 600 for a more refined editorial display weight (Newsreader scales better than NSK at lower weights).
[0.7.8.1] - 2026-05-11 — Review session typography + content-aware Hangul hero
Changed
- Review session typography — FSRS review card surfaces now share the editorial voice. Card-type chip (RECOGNIZE / RECALL) and rating-button labels (AGAIN / HARD / GOOD / EASY) switched to Fraunces upright small-caps with letter-spacing. Card-front prompts stay in Noto Serif KR (handles both Korean and Latin cleanly). Card counter ("3 / 12") switched to Geist Mono for numeric feel. Pause link now Fraunces small-caps. Review hint at card bottom now italic Newsreader. Rating-number keyboard shortcuts (1 / 2 / 3 / 4) switched to Geist Mono. The review heading (post-session "All caught up", paused state) switched from NSK bold to Fraunces upright 500.
- Hangul hero on grammar entries — content-aware sizing. The hero now adapts to title length instead of a fixed
clamp(96px, 12vw, 160px). Short titles like~지만still land at ~112px (impactful typographic anchor); longer titles like~아/어야 되다 / ~아/어야 하다step down to ~56px and fit on one line without overflowing the content column. Computed server-side fromtitle.length(whitespace and slashes count half because they render narrower than Hangul glyphs), bound between 56px–112px, passed to the page as a CSS custom property. Mobile caps at 72px regardless. Addstext-wrap: balancefor cleaner wrap distribution when a title does need two lines.
[0.7.8.0] - 2026-05-11 — Reader + Tutor typography + grammar library polish
Extends the dual-serif typography to the remaining reading-content surfaces. Reader and Tutor now share the same editorial voice as grammar entries.
Added
.eyebrowutility class inglobal.css— Fraunces upright small-caps with 0.06em letter-spacing. Apply anywhere a "label, all-small-caps, letter-spaced" treatment is wanted; size + colour layer on top via Tailwind utilities. Used across Reader, Tutor, and grammar library to replace the longfont-ui text-[Npx] font-semibold uppercase tracking-widestpattern that recurred throughout.
Changed
- Grammar library search input — now Newsreader 16px with italic placeholder. The library reads as content rather than form.
- Grammar library level filter pills (All / Beginner / Intermediate / Advanced) — Fraunces upright small-caps with letter-spacing via a new
.grammar-level-pillclass. Reads as editorial filter chrome rather than generic chip UI. - Reader library (
/reader) — Reader page heading switched from NSK bold to Fraunces upright 500 weight. Section eyebrows (New passage, Your passages), char-count meta, and passage card meta all swapped fromfont-ui ... uppercase tracking-widestto.reader-section-eyebrow/.reader-meta-label(Fraunces small-caps). Description paragraph + loading/empty states now Newsreader body. - Reader passage view (
/reader/[id]) — Library back link now uses the shared.back-linkclass. Sidebar "Grammar in this passage" heading and popover labels (Grammar, POS, Example, Close) all switched to Fraunces small-caps via.reader-section-eyebrow/.reader-meta-label. Grammar form names in sidebar moved to Noto Serif KR (editorial form display); index numbers and ×count to Geist Mono (numerals). Grammar-link descriptions, lookup glosses, and loading text now Newsreader body. Korean passage prose itself stays Pretendard (optimized for Korean reading). - Tutor workspace (
/tutor) — Empty-state copy, analysis-panel loading/error messages, blind-spot descriptions, and study-suggestion descriptions all now Newsreader body. Tab labels, draft chips, action buttons, char-count meta, "remaining today" badge, and grammar-category tags all use the new.eyebrowutility (Fraunces upright small-caps). Title input and Korean textarea still use Pretendard (form input chrome).
Internal
- Reader and Tutor live inside large Preact components (
ReaderView.tsx823 lines,WritingEditor.tsx,AnalysisPanel.tsx,TutorWorkspace.tsx). The rollout was surgical —.eyebrowutility consolidated the ~12 repeatedfont-ui ... uppercase tracking-widestpatterns into one class without touching layout or interaction.
[0.7.7.0] - 2026-05-11 — Reading-content typography rolled out across pages
The Newsreader-body + Fraunces-labels typography system that lives on grammar entries now extends to the other reading-content surfaces: the long-form changelog, the 404 page, and grammar list card descriptions. UI chrome (auth forms, dashboard, settings, FSRS review) intentionally stays Pretendard — serif body on form inputs reads off-key.
Changed
.prosestyle (used bychangelog.astroand any future long-form markdown surface) switched from 16px Pretendard body + Noto Serif KR/to 17px Newsreader body + Fraunces upright/. Mirrors the grammar entry typography so changelog reads with the same editorial voice as the reference.- Grammar list cards (
src/components/GrammarList.tsx) — entry definition under each card now in Newsreader 15px instead of Pretendard 14px. The Korean title stays in Noto Serif KR (the entry's identity); only the English meaning text moved. - Changelog page — back link tightened to use the shared
.back-linkclass (Fraunces upright small-caps), and the redundantfont-kron the prose section dropped. - 404 page — heading switched from Noto Serif KR bold to Fraunces upright 500 weight; the descriptive paragraph now Newsreader 17px.
Internal
- New
scripts/multi-page-screenshots.mjsfor capturing the typography rollout across/,/changelog, and/404at 1280px during design review.
Not yet rolled out
- Reader pages (
/reader,/reader/[id]) and Tutor (/tutor) — these are large Preact components with their own typography decisions; they need a focused pass rather than a sweeping override. - Dashboard, settings, auth pages, FSRS review — UI chrome surfaces. Pretendard stays. (Intent: serif body on form inputs reads off-key.)
[0.7.6.2] - 2026-05-11 — Back link in Fraunces small-caps + hero vertical rhythm
Changed
- Back link ("← Back to Grammar" / "← Back to your passage") switched from Pretendard 14px to Fraunces upright small-caps 14px, matching the TOC rail and section heading typeface. Treats the back link as editorial chrome rather than a body affordance.
- Hero vertical rhythm opened up. Hangul → voice line gap 24px → 40px; voice line → TL;DR gap 24px → 40px. The Hangul hero is visually massive (clamp 96–160px); the prior 1.5rem gap made the voice line crowd the hero. 2.5rem on both sides gives the editorial thesis room to breathe.
[0.7.6.1] - 2026-05-11 — TOC rail in Fraunces small-caps
Changed
- Left-rail TOC on grammar entries switched from Pretendard 13px to Fraunces upright small-caps 14px (
letter-spacing: 0.05em). The rail now mirrors the section-heading typeface so it reads as a mini-index of the same labels: TL;DR · USE & MEANING · EXAMPLES · SEE ALSO · SOURCES.
[0.7.6.0] - 2026-05-11 — Newsreader body serif + Fraunces labels (dual-serif typography)
Body prose on grammar entries now renders in Newsreader — an NYT-Imperial-flavoured news serif designed for screen body reading. Fraunces stays for the editorial-label layer and the voice line.
Added
- Newsreader Variable (
@fontsource-variable/newsreader, both upright and italic axes) as the body serif for grammar entries. Calmer than Fraunces, designed for screen body reading at length. Mixed-script: per-character fallback routes Hangul to Noto Serif KR so both scripts stay serif and the page texture stays coherent. --font-serif-bodyCSS variable ('Newsreader Variable', 'Noto Serif KR Variable', Georgia, …) for the body workhorse on grammar entries.
Changed
- Body prose, TL;DR body, Use body, source citations all switch from Pretendard to Newsreader. The
style inside body prose stays in the Newsreader family (italic axis) rather than jumping to Fraunces italic, so paragraphs read with uniform texture. Example English (still italic, parallel-translation convention) and book titles also move to Newsreader italic. - Fraunces retains the editorial-label layer: section headings, Use 1/2/3 eyebrows, TL;DR label, body prose subheadings (
## Compared to ~(으)ㄴ/는데), source-legacy label. And the voice line under the Hangul hero — italic Fraunces still carries the editorial thesis (the moment where Fraunces character has the most leverage). - Non-grammar pages (dashboard, login, settings, etc.) unchanged — they still use Pretendard as the body font.
Internal
- DESIGN.md typography section restructured to document the dual-serif split (Fraunces = labels + voice, Newsreader = body). Bundle ceiling raised from 180KB → 220KB Latin to accommodate the second editorial serif (~70KB Newsreader Latin subset on top of ~92KB Fraunces). Total Latin-side payload on grammar entry pages now ~193KB; fail any PR pushing past 250KB.
mockups/body-font-compare.htmladded as a design reference — same~지만content rendered in both fonts side-by-side, served by Google Fonts CDN for the comparison.
[0.7.5.0] - 2026-05-11 — Broader Fraunces for editorial labels + Yeon & Brown attribution discipline
A typography + voice iteration on grammar entries.
Added
- Fraunces upright axis loaded (
@fontsource-variable/fraunces/wght.css) in addition to the existing italic axis. Both share theFraunces Variablefamily —font-style: normalyields upright glyphs,italicyields the italic axis. Adds ~46KB Latin-subset to cold-load.
Changed
- Editorial labels now in upright Fraunces. Section headings (TL;DR, Use & Meaning, Examples, See also, Sources), Use 1/2/3 eyebrows, the TL;DR label, body prose subheadings (
## Compared to ~(으)ㄴ/는데, etc.), and the legacy Source label all switch from Pretendard / Noto Serif KR to upright Fraunces. Italic Fraunces still carries voice (thesis line under the hero, example translations, semantic, book titles); upright Fraunces carries labels. Pretendard remains the body workhorse. - Section heading + use eyebrow + prose subheading sizes bumped alongside the typeface swap (Section 16 → 18, Use 14 → 15, Prose h2 22 → 24, h3 19 → 20) so Fraunces reads at its more flattering scale.
- Yeon & Brown attribution discipline applied to the four flagship entries (
~것 같다,~지만,~(으)니까,~거든요). Lazy inline attribution ("Y&B note…", "Per Y&B…", "Y&B's textbook example…") removed from TLDRs,uses[].body, and body prose. Direct scholarly attribution kept where it carries information (e.g., "Following Lukoff & Nam 1982 and Sohn 1992…", "a use first called out by King & Yeon 2002"). The Sources block in each entry's frontmatter carries the textbook citation; the reader prose now reads as a grammar reference rather than a Y&B summary.
Internal
- DESIGN.md unchanged for this PR — typography token names stay the same; the
--font-italic-displayCSS variable is now used for both italic *and* upright Fraunces withfont-styletoggling the axis. Documented at the top of the entry-layout block in global.css.
[0.7.4.5] - 2026-05-10 — Drop eyebrow row, vocabulary section, and Entry NN cross-ref tag
Removed
- Tier · Level · Entry NN eyebrow row removed from grammar entries. The labelling scheme (
Tier 2 · Intermediate · Entry 61) was unsourced — no official documentation backs the tier or entry-number conventions, so surfacing them as authoritative chrome misled readers. The hero now opens directly with the Hangul.entryNumberandlevelremain in the schema (used by the validator and StudyButton card generation) but are no longer rendered. - Vocabulary section removed from the entry layout. The standalone two-column table didn't carry its weight as a learning surface — vocab still flows into the StudyButton's card generator via the entry's
vocabfrontmatter, just not as a duplicate page block. (Entry NN)parenthetical dropped from See also cross-references for the same labelling-discipline reason. Cross-refs now read as→ ~formonly — the Korean form is the stable identifier.
Internal
src/components/grammar/EyebrowRow.astrodeleted (no callers).e2e/grammar.spec.tsVocabulary visibility assertion replaced with a Use & Meaning visibility assertion.- Unused CSS pruned (
.eyebrow-row,.eyebrow-sep,.eyebrow-entry,.entry-vocab-*,.entry-crossref-number).
[0.7.4.4] - 2026-05-10 — Hero polish: right-aligned eyebrow + voice-line breathing room
Changed
- Eyebrow row right-aligned.
Tier · Level · Entry NNnow hugs the right edge of the content column, mirroring the page-header Study button and giving the Hangul hero a single strong left-edge anchor. - Voice line gets a top margin. The italic-Fraunces thesis under the Hangul hero now has 1.5rem of space above it, separating it visually from the hero.
[0.7.4.3] - 2026-05-10 — Content fix: tighten ~것 같다 definition (no Korean leak)
Fixed
~것 같다definition no longer leaks the Korean answer. The previous text embedded감기 걸린 것 같아요 = 'I have a cold'directly in thedefinitionfield — which is what FSRS recall cards show on the English side, defeating the recall point. New definition:"I think / it seems (everyday spoken hedge — softens both genuine conjecture and definite statements)". The textbook example moves to the TL;DR where it belongs as a pedagogical illustration.- TL;DR redundancy reduced. Drops the redundant intro line that overlapped with the definition; now leads with the structural insight (modifier + 것 + 같-) and Y&B's two-uses dichotomy.
[0.7.4.2] - 2026-05-10 — Page header + section TOC rail on grammar entries
Layout iteration on the new grammar entry pages.
Added
- Section TOC rail. Sticky left-rail navigation lists every section that's populated for the current entry: TL;DR, Use & Meaning, Vocabulary, Examples, See also, Sources. Smooth-scroll anchors via the global
scroll-behavior: smoothsetting. Hidden below 1024px (no horizontal real estate); content column reflows to full width.
Changed
- Study button moves to the page header. Previously inside the hero block (sometimes sticky-bottom on mobile). Now a flex space-between strip at the top of the page:
← Back to Grammaron the left, Study button on the right — visible at eye level when the page first loads and persistent above the hero. - Examples breathe further. Inter-example gap 2.25rem → 3rem on desktop, 1.75rem → 2.25rem on mobile. Per-example Korean → English line gap 0.4rem → 0.6rem.
[0.7.4.1] - 2026-05-10 — Typography iteration on the new grammar entry layout
CSS-only follow-up to v0.7.4.0. Tightens the italic discipline so italic carries signal again, bumps body sizes for reading comfort, and gives examples vertical breath.
Changed
- Italic discipline. Italic now appears in only four places on a grammar entry: the voice line under the Hangul hero, English translations under examples, semantic
in body prose, and book titles in citations. Previously the eyebrow row, TL;DR label, section headings, Use 1/2/3 eyebrows, body subheadings, and the entire Sources list were all italic Fraunces — making nothing stand out. Eyebrows and section headings now render in upright Pretendard small-caps with letter-spacing; body subheadings (## Compared to ~(으)ㄴ/는데) render in upright Noto Serif KR. - Body sizes bumped for reading comfort. Voice line 19 → 22px, TL;DR body 15 → 17px, Use body + prose 15 → 17px, section headings 14 → 16px, vocab Korean 17 → 18px and English 15 → 16px, example Korean 17 → 19px and English 15 → 17px, cross-ref title 16 → 18px, prose h2/h3 20/18 → 22/19px.
- Examples breathe. Gap between examples 1.5rem → 2.25rem on desktop (1.75rem mobile). Per-example: Korean → English line gap 0.25rem → 0.4rem.
[0.7.4.0] - 2026-05-08 — Grammar entry layout refactor: Use & Meaning, See also, Sources (PR3 of UI iteration roadmap)
PR3 of the v0.7.x UI iteration roadmap — the visual payoff release. Grammar entry pages get a full layout refactor in the linguistics-paper aesthetic that PRs 1–2 set up. New "Use & Meaning" structured-uses section, italic-Fraunces small-caps eyebrows, large-weight-200 Hangul hero, italic-Fraunces voice line, "See also" cross-references between entries, and a proper "Sources" bibliography block.
Added
- Structured
usesfield on grammar entries. Newuses: [{ title, body }]frontmatter array lets entries break their meaning down into a numbered Use 1 / Use 2 / Use 3 list with a short noun-phrase title and 1–3 sentences of body prose. Three flagship entries migrated to drive the new layout:~지만(3 uses — different-subject contrast, same-subject acknowledged-but-outweighed, polite openers),~(으)니까(2 uses — causation, discovery),~거든요(3 uses — reason, source-of-information, standalone pushback). - Structured
seeAlsofield on grammar entries. NewseeAlso: ["slug", "~slug"]frontmatter array renders a "See also" block at the bottom of each entry with→ ~form (Entry NN)typeset cross-references. Authors can write the bare filename basename or the tilde-prefixed form interchangeably. The build-time validator (scripts/validate-content.ts) fails the build on any dangling reference or self-reference. Three flagship cross-refs populated:~지만 → ~(으)ㄴ/는데,~(으)니까 → ~아/어서 + ~거든요,~거든요 → ~(으)니까. - Five new presentational components in
src/components/grammar/—EyebrowRow.astro(italic Fraunces small-caps tier · level · Geist Mono entry tag),UseSection.astro(Use N · title eyebrow + body),Example.astro(3-line layout: Hangul / Geist Mono gloss / italic Fraunces English, 2-line fallback when gloss absent),CrossRef.astro(split-typography cross-reference renderer with(missing)fallback),SourcesBlock.astro(italic Fraunces citation lines with Geist Mono section/page locators). firstSentence()voice-line helper atsrc/lib/voice-line.ts— splits an entry'sdefinitionfield on the first sentence terminator (.,!,?) so the layout can render the thesis under the Hangul hero in italic Fraunces. Tolerates closing quotes/parens after the terminator (X said 'foo.' Then Y.→X said 'foo.') so real entries like~ㄴ후에(chronological 'after.' Three forms…) split cleanly. 16 vitest specs cover terminators, parentheticals, Korean periods, and quote-inside-period boundaries.extractSeeAlsoRefs()parser atsrc/lib/content-validation.ts— pure helper exposed for vitest. Handles both inline (seeAlso: ["a", "b"]) and block-form (- "a"\n - "b") YAML, plus multi-line inline arrays, mixed quote styles, unquoted bare slugs, and tilde stripping. 10 specs cover the regex behavior the validator depends on.- Build-time
seeAlsointegrity check.scripts/validate-content.tsnow fails the build if anyseeAlsoslug doesn't resolve to another entry, or if an entry references itself. Matches the existingentryNumbercollision check and runs on everybun run build.
Changed
- Grammar entry route (
src/pages/grammar/[...slug].astro) rebuilt around the new layout. Reading order: eyebrow row → Hangul hero (NSK weight 200,clamp(96px, 12vw, 160px)) → italic-Fraunces voice line → Study button → TL;DR (plain-bordered, no persimmon) → Use & Meaning section (iteratesuses[]then continues with the MDX body) → Vocabulary → Examples (3-line) → See also → Sources (or legacysourcefooter for entries that haven't been re-cited yet). - Site-wide focus ring changed from 2px dashed accent to 2px solid
--accent-foreground, offset-2. Aligns with the linguistics-paper aesthetic and the persimmon discipline rule (accent reserved for ≤2 chrome positions per page). DESIGN.md updated to record the new sanction. - Three flagship MDX bodies refactored.
~지만,~(으)니까,~거든요— per-use sections pulled out into the new structureduses[]array; remaining body keeps the framing prose, comparisons, attachment rules, and tips.~지만's "Compared to ~(으)ㄴ/는데" / "Compared to 하지만" headings demoted from##to###so they sit under the section'scleanly. - Heading hierarchy on entry pages unified — Use & Meaning, Vocabulary, Examples, See also, and Sources all render as
(sibling section headings under the entry'sHangul hero), so screen-reader navigation reports a clean outline. - Persimmon discipline reapplied.
.entry-use-eyebrow(which stacks 2–3 times per entry) takes the editorial gray--accent-foregroundinstead of the persimmon accent, leaving the Study button and FSRS Good state as the page's only persimmon chrome positions.
Fixed
scripts/validate-content.tsseeAlso parser now matches multi-line inline YAML arrays (seeAlso: [\n "a",\n "b"\n]). The previous\[.*?\]regex required no newlines inside the brackets and silently fell through to the block-form alternative on multi-line arrays, skipping integrity checks for that entry..entry-source-legacy-labelnow usesfont-style: italicto match the rest of the eyebrow stack. The previousfont-style: normalpaired with the italic-axis Fraunces font produced synthetic-upright Fraunces, which DESIGN.md's typography section warns against.SourcesBlockcomma separators are nowaria-hidden="true"so screen readers don't verbalize "comma" between bibliographic fields.
[0.7.3.0] - 2026-05-08 — Content schema migration: stable entry numbers + structured sources (PR2 of UI iteration roadmap)
PR2 of the v0.7.x UI iteration roadmap. Schema-only release: gives every grammar entry a stable canonical number, adds a structured bibliography field, and prepares example data for the upcoming three-line layout. No user-visible UI change yet — the new fields surface in PR3 (entry layout refactor).
Added
- Stable
entryNumberon every grammar entry. Each of the 62 grammar entries now has a canonical number assigned by alphabetical filename order. Numbers are stable across content additions:~지만is Entry 61 forever, even if new entries are added before it. Once retired, a number is never reused. - Structured
sourcesarray. Newsources: [{ textbook, title, pages?, section? }]frontmatter field for proper bibliography rendering in the upcoming Sources block (PR3). Three flagship entries populated to drive the layout work:~거든요(Entry 18),~(으)니까(Entry 55),~지만(Entry 61), each citing Yeon & Brown's *Korean: A Comprehensive Grammar* (2nd ed.) at the section level. Existing legacysource(singular, free-form string) kept for backward compatibility on entries that haven't been re-cited yet. glossfield on examples. Optional Leipzig-style morpheme gloss per example (e.g.,expensive-CONNECTIVE delicious-be-POL) for the upcoming three-line example layout. Empty string defaults; the renderer will fall back to a two-line layout when absent.scripts/validate-content.ts— build-time validator for cross-collection invariants Astro's per-entry Zod can't catch on its own. Currently enforces uniqueentryNumberand provides a hook for cross-reference checking when PR3 introduces(Entry NN)syntax. Wired into the build script as the first step (bun run validate:content && astro check && astro build); fails the build with clean per-file diagnostics on conflict.scripts/migrate-entry-numbers.ts— one-shot migration kept in the repo for reference and future content additions. Idempotent (skips entries that already have a number); assigns the next free positive integer to each new entry.
Changed
- Build script now runs the content validator before
astro check && astro build. Adds <100ms to a clean build but catches duplicateentryNumberconflicts before they reach prod.
[0.7.2.0] - 2026-05-07 — Typography foundation: Geist Mono + Fraunces (PR1 of UI iteration roadmap)
PR1 of the v0.7.x UI iteration roadmap. Foundation-only release: adds new font primitives and design tokens that upcoming PRs will use to refactor grammar entry typography. No user-visible UI change in this version.
Added
- Geist Mono Variable (
@fontsource-variable/geist-mono) — reserved for numeral roles in the upcoming refactor:(Entry NN)cross-reference parentheticals, morpheme gloss line under examples, dashboard stat numbers. Latin subset only at cold-load (~31KB compressed woff2 viaunicode-range). - Fraunces Variable (
@fontsource-variable/fraunces, italic axis only viawght-italic.css) — reserved for italic-display editorial roles: italic small-caps eyebrows, italic voice line under entry headers, italic English in three-line examples, italic source citations. Latin subset only at cold-load (~46KB compressed woff2). Required because Noto Serif KR has no true italic; synthetic browser-italic looks bad at display sizes. --accent-foregroundtoken (light:#2a2a2a, dark:#d8d6d0) — near-foreground color for links, hovers, and focus rings where persimmon used to live. Part of upcoming persimmon-discipline refactor (≤2 chrome positions per page).font-monoandfont-italic-displayTailwind utilities — wired up via@theme inlineso upcoming components can reference the new families.- TODOS.md entries logging Phase 2 (cmd+K, j/k power-user surface) cut to TODOS as P2 blocked-by-audience-signal, and a competitive-scan task (Naver Korean Dictionary, HowToStudyKorean, TTMIK, Bunpro) blocking Phase 3 differentiator features only. Both entries land here because /autoplan generated them when reviewing the v0.7.x design plan.
Documentation
- DESIGN.md — Typography section gains entries for Fraunces (italic-display roles, Hangul split-typography rule for cross-references) and Geist Mono (numeral-only role). New
--accent-foregroundrow added to both color tables. Bundle-cost ceiling (≤180KB compressed Latin font payload, fail-PR threshold 250KB) documented.
[0.7.1.0] - 2026-05-07 — Dashboard page + sign-in form polish + user dropdown menu
Iteration on the v0.7 auth/UX surface. Splits the personal study stats off the home page so the grammar listing reads as a clean browse, demotes the magic-link toggle to a secondary action, and consolidates signed-in header items into a single avatar dropdown.
Added
/dashboardpage. Personal study stats (cards due, minutes, sessions, next review) live here now. Login redirects to/dashboardby default. Direct visit redirects to/loginif signed out. Signup still lands on/so new users can pick their first grammar point.- User dropdown menu. Replaces the email + Settings + Log out cluster in the header. Trigger is a small accent-colored avatar with the user's email initial; the panel shows email, Dashboard, Settings, Log out. Closes on outside click, Escape, or route swap. Mobile hamburger menu also gains Dashboard + Log out entries.
Changed
- Sign-in form simplified. Magic-link tabs at the top of the login card replaced with a stacked secondary "Email me a sign-in link instead" button below the primary submit. Default mode is password; one click swaps the form into magic-link mode.
- Home page (
/) is browse-only. DashboardStats removed from the index — heading + grammar list only. - New-user welcome message in DashboardStats now links to
/to browse grammar (no longer says "below").
Fixed
- E2E test infra.
bun run test:e2e:fullnow disables email features for the test run (RESEND_API_KEY=""), unblocking session-based tests that broke when v0.7.0.0 addedrequireEmailVerification. Grammar fixture switched from~아/어서(no longer in the first-20 listing after recent content batches) to~거든요. Header-nav selector scoped to exclude the mobile menu so the strict-mode collision goes away. - Logout selector. Switched from the duplicate-id
#logout-linkto.logout-linkclass so desktop dropdown and mobile hamburger menu can both fire the logout flow without invalid HTML.
[0.7.0.0] - 2026-05-07 — Auth: email verification, password reset, magic-link sign-in, confirm-password
PR A of the auth-improvements track. Adds the email-dependent auth features that were missing from the original better-auth setup, plus a confirm-password field on signup. Social providers (PR B) ship separately.
Added
- Email verification on signup. Hard-mode (
requireEmailVerification: true): users must click the link before they can log in. IncludesautoSignInAfterVerificationso first-time users land authenticated after verifying. - Password reset flow. New
/forgot-passwordpage (request a reset link by email) and/reset-passwordpage (set new password from email link). Reset tokens valid for 1 hour by default. Confirmation message is identical regardless of whether the email matches an account — prevents probing for registered emails. - Magic-link sign-in. New "Email me a link" toggle on the login form. Sends a one-time sign-in link valid for 5 minutes. Useful for users who've forgotten their password or want passwordless login.
- Confirm-password field on signup. Pure client-side validation — submit blocked if the two fields don't match. Surfaces error inline.
verify-emaillanding page. Friendly success/failure message after the email link is clicked. Better-auth's verification endpoint redirects here.- Resend integration for transactional email. New
src/lib/email.tswraps the Resend SDK with a singlesendEmail(...)helper plus three HTML templates (verification, reset, magic link) styled to match the design system.
Configuration
Two new env vars on Railway:
RESEND_API_KEY— required for any email-dependent feature to work. Get from [resend.com](https://resend.com) (free tier covers 100 emails/day).EMAIL_FROM— sender address. Defaults toonboarding@resend.devif unset (Resend's testing sender; deliverability is poor — verify a domain in production).
RESEND_API_KEY presence. When the key is unset:
requireEmailVerificationdefaults tofalseso basic signup still works (users can log in immediately, no verification needed).- The
magicLinkplugin is not loaded. sendResetPasswordis undefined.- The login form hides the magic-link toggle and forgot-password link.
RESEND_API_KEY is configured — basic email/password works as-before, and the new features activate the moment the env var lands and the service restarts.
Changed
src/lib/auth-client.tsadds themagicLinkClientplugin so the form can callauthClient.signIn.magicLink(...).src/components/AuthForm.tsxrewritten to support three flows on one form: signup with confirm-password, password login, magic-link login (toggleable on the login screen).src/layouts/Base.astroexposeswindow.__AUTH__.emailEnabled(computed from server-sideRESEND_API_KEYpresence) so the client can hide email-dependent UI affordances.
Test coverage
No automated test for the email flows themselves (would require mocking Resend + better-auth's token state). Type-check clean, all 224 existing unit tests pass.
[0.6.1.1] - 2026-05-06 — Dashboard fixes: "next review" and "sessions" counters
Two long-standing bugs on the home dashboard, previously attempted but unfixed.
Fixed
- "Next review" tile no longer permanently shows "—". The dashboard was suppressing this value whenever the user had any due cards, so active learners with a backlog never saw it. Now shows the earliest due time at all times —
nowwhen reviews are pending, otherwise the relative time until the next card is due. - "Sessions" counter no longer stuck. Was tied to the local IndexedDB backups table (capped at 5 rows, plus a separate persistence bug that left users at "1" indefinitely). Now derived server-side from
review_log.reviewedAtclusters separated by gaps of more than 30 minutes, via a newGET /api/stats/sessionsendpoint. Reflects activity across devices and isn't capped.
Added
src/lib/sessions.tswithcountSessions()— pure clustering helper used by both the new endpoint and the offline-guest fallback path. 7 unit tests cover empty, single, sub-gap, supra-gap, threshold, unsorted, and multi-day cases.GET /api/stats/sessionsendpoint (auth required), backed by an existing index on(userId, reviewedAt). 4 endpoint tests cover unauthorized, zero-state, clustering, and unsorted-input cases.
Changed
LocalDataProvider.getSessionCount()(guest path) now derives from local Dexie reviewLogs instead of the backups table.SyncingDataProvider.getSessionCount()calls the server endpoint and falls back to local-derived on network failure.
Known issues
- Local IndexedDB backup persistence is unreliable in production — see [#39](https://github.com/jon-karlsen/korean-v2/issues/39). Decoupling the dashboard from
db.backups.count()makes this no longer user-visible on the dashboard, but the underlying issue affects the "stale backup" warning and rolling-snapshot recovery. Tracked separately.
[0.6.1.0] - 2026-05-06 — Tier 2 intermediate grammar batch (7 new entries)
Adds the high-frequency intermediate patterns Y&B treats as everyday-spoken / everyday-written staples that complete what was started in v0.6.0.0's Tier 1 batch.
Added — seven net-new entries
- ~아/어 있다 (Y&B §4.3.3.1) — continuous-state aspect, the partner of ~고 있다 (continuous action). Y&B's headline contrast: 앉아 있다 = "in the seated state" vs 앉고 있다 = "in the act of sitting down." Used for body positions, open/closed states, installation passives, and the motion-verb "has gone / has come" use (가 있다 / 와 있다). Critical pointer: do NOT use ~고 있다 for standing/sitting/lying — those go through this construction. Honorific form 계시다.
- ~아/어 주다 (Y&B §5.1.12) — benefactive auxiliary, "do for someone." Beneficiary marked with ~에게/한테 for tangible benefit, ~을/를 위해 for intangible favor. Adding/removing ~주다 can flip a sentence's meaning entirely (빌리다 = "borrow" vs 빌려 주다 = "lend"). Honorific form ~아/어 드리다 swaps when beneficiary outranks speaker, with ~에게/한테 → ~께. Heavy use in politeness expressions (와 주셔서 감사합니다 = "thanks for coming").
- ~지 말다 (Y&B §4.2.3) — negative imperative auxiliary. Required for negating commands and proposals (short ~안 and long ~지 않다 / ~지 못하다 cannot do this). ㄹ-irregular (말 → 마). Common forms 마세요 / 마 / 마십시오 / 맙시다 / 말자. ~지 말고 ("instead of") and noun-direct 말고 ("rather than" / "except for") covered.
- ~(으)면 안 되다 (Y&B §7.5.1.5) — "shouldn't / not allowed to." Structurally ~(으)면 + 안 + 되다 (literally "if you ..., it does NOT do"). Pairs with ~지 말다 — both express prohibition but with different framings: ~지 말다 is a direct command, ~(으)면 안 되다 frames the action as prohibited / not permitted. Opposite-permission counterpart: ~아/어도 되다.
- ~(ㄴ/는)다고 하다 (Y&B §10.2.1) — indirect quotation. Plain-style statement ending + ~고 + reporting verb (하다, 그렇다, 듣다). Covers all six quotation shapes (action ~ㄴ다고 / ~는다고, descriptive ~다고, past ~았/었다고, future ~겠다고, future ~(으)ㄹ 거 → ~거라고, copula ~(이)라고 — note the irregular *NOT* ~이다고). Includes the self-introduction form 한나라고 합니다.
- ~(으)ㄴ/는 한 (Y&B §8.2.41) — "as long as / as far as / to the extent that." Modifier + Sino-Korean noun 한 (限, "limits, bounds"). Headline collocation 가능한 한 ("as ... as possible"); also 힘이 닿는 한, 제가 아는 한 (epistemic hedge). Distinguished from the conditional ~(으)면 — this construction sets the *boundary of a claim*, not a hypothetical condition.
- ~더라도 (Y&B §7.2.6) — strong concessive ending. Stronger than ~(아/어)도 — frames the first clause as hypothetical rather than fact. Often pairs with 아무리 ("however much"). ~다고 하더라도 = "even if we grant that..." (rhetorical). Concessive cluster now disambiguated: ~지만 (direct contrast), ~(으)ㄴ/는데 (soft background), ~(으)ㄴ/는데도 (despite a known fact), ~더라도 (strong hypothetical concession).
Added — integration test coverage
All 7 new entries have Kiwi-validated canonical-example tests. Test suite gains 7 pattern-validation tests (217 → 224 total).
Library size: 56 entries → 63 entries.
[0.6.0.0] - 2026-05-04 — Tier 1 foundational grammar batch (6 new entries)
After three disambiguation passes that closed every meaningful collision in the existing library, this release fills the Tier 1 foundational gaps — the basic patterns every Korean learner uses constantly but were missing from the library.
Added — six net-new entries
- ~아/어야 되다 / ~아/어야 하다 (Y&B §7.5.7.1) — the standard "have to / must" construction. Covers Y&B's necessity-vs-obligation distinction (되다 leans necessity, 하다 leans obligation), the past-tense "should have done" use, and the two everyday contractions ~아/어야지요 (shared-knowledge) and ~아/어야겠어요 (future/conjecture).
- ~고 있다 (Y&B §4.3.3.2) — the progressive aspect. Six learner-trap differences from English progressive: optional in Korean (not obligatory), required with telic verbs in past tense to mark incomplete action (찾았어요 vs 찾고 있었어요), no future-time reading, works with stative/cognitive verbs (알고 있어요 = "I already know"), allowed in imperatives ("stay waiting"), and the wearing-verbs ambiguity. Critical pointer that ~아/어 있다 (not this) is used for standing/sitting/lying.
- ~아/어 보다 (Y&B §5.1.8) — "try doing." Five uses: attempt, sampling (often with 한 번), specific-purpose attempt (English drops "try"), "have you ever?" (with past tense), and the major softening use that turns imperatives into "why don't you...?" (앉아 보세요). Includes the 묻다 + 보다 → 물어보다 fusion and set-phrase greetings (먼저 가 보겠습니다).
- ~(으)면 (Y&B §7.5.1) — the foundational conditional. Three present-tense uses (if / when for certain future events / "given that, seeing as") and two past-marked ~(았/었)으면 regret uses (past counterfactual interchangeable with ~(았/었)더라면, vs present-state regret where ~더라면 cannot substitute).
- ~기 전(에) (Y&B §2.2.4.10) — "before doing." Three particle variants (~기 전에 connective, ~기 전의 adnominal, ~기 전이다 copular) plus the noun-direct form (점심 전에). Critical asymmetry note: "before" uses the nominalizer ~기 (action not yet realized), while "after" uses ~(으)ㄴ (state/result modifier — completed action).
- ~(으)ㄴ 다음(에) / 뒤(에) / 후(에) (Y&B §8.2.9) — "after doing." Three interchangeable forms (다음 colloquial, 뒤 neutral, 후 formal/written) — Y&B treats them as one unified pattern with register variation.
Fixed
~는 동안(에) / ~는 사이(에)pattern bug discovered during integration test backfill. The pattern field usedNNB(matching Y&B's analysis of 동안 as a bound noun), but Kiwi tokenizer tags 동안 asNNG(general noun). The pattern wasn't actually matching real text. NowNNG, validated by integration test against the canonical example.
Added — integration test coverage backfill
Five entries shipped in earlier releases (v0.5.13.0 ~(으)니까, v0.5.14.0 ~것 같다, v0.5.15.0 ~다(가) and ~는 동안(에)) had been built without canonical-example integration tests. All now validated against real Kiwi tokenizer output. Plus the six new Tier 1 entries — the test suite gains 11 new pattern-validation tests in this release (29 → 31, plus 5 backfills covered prior gaps).
Library size: 50 entries → 56 entries.
[0.5.15.0] - 2026-05-04 — Grammar disambiguation pass: temporal cluster + ~다(가) + ~는 동안(에)
Added
- ~다(가) / ~았/었다(가) — net-new entry sourced to Y&B §7.3.11. The transitional marker was a real gap in the library — Y&B cross-references it across the chapter (it's a component of ~(으)려다가, ~(으)ㄹ까 하다가, etc.). Full coverage of the load-bearing distinction: without past tense the first action is interrupted or ongoing ('while running, I fell'); with ~았/었 the first action is completed and the second often introduces an unexpected consequence ('drank soju, then passed out') or a reversible action ('bought, then sold'). Includes the 갔다 오- ('go and come back') leave-taking idiom, the repeated back-and-forth ~다가 ~다가 하다 pattern, and the infinitive + 다 fixed expressions (내려다 봐요 'look downwards').
- ~는 동안(에) / ~는 사이(에) — net-new entry sourced to Y&B §8.2.12. Korea's 'while' pattern for two different subjects acting at the same time was missing — the headline contrast with ~(으)면서, which requires the same subject. Covers the 동안 (longer durations) vs 사이 (shorter intervals) distinction, the occasional state/result ~(으)ㄴ form with motion verbs (학교에 간 사이에 'while we were away at school'), and the noun-direct attachment (3일 동안).
Changed
- Disambiguated the temporal cluster
definition:lines so each of the six entries' first phrases uniquely cues the form. The cluster spans both "when" (~(으)ㄹ 때, ~자(마자), ~더니/~았/었더니) and "while" (~(으)면서, ~다(가), ~는 동안(에)) families:
The body prose of the four existing entries is unchanged — already audited in earlier batches. The cluster is now navigable from the front of a reverse-recall card: same-subject simultaneous, different-subject simultaneous, and interrupted/transitional all land on different cues.
[0.5.14.0] - 2026-05-03 — Grammar disambiguation pass: conjecture family + ~것 같다
Added
- ~것 같다 — net-new entry sourced to Y&B §8.2.3. The everyday all-purpose conjecture and softener in spoken Korean was a real gap in the library. Full coverage of Y&B's two uses (literal "it seems that..." vs the figurative "I think..." hedge), all six modifier shapes (prospective, present dynamic, state/result, continuous past, discontinuous past, prospective past), and the noun-attached form (여름 같아요). Includes the King & Yeon 2002 hedging insight (감기 걸린 것 같아요 said by a coughing speaker who's actually sure they have a cold).
Changed
- Disambiguated the conjecture family
definition:lines so each entry's first phrase uniquely cues the form. Previously ~나 보- and ~(으)ㄹ/(으)ㄴ/는 모양이다 both opened with "It looks like / It seems like," collapsing into the same gloss space. New cues:
The body prose of the two existing entries is unchanged — already audited in earlier batches. This PR is scoped to adding ~것 같다 and rewriting the definition: field on the three entries.
[0.5.13.1] - 2026-05-03 — Grammar disambiguation pass: intent / purpose family
Changed
- Disambiguated the intent / purpose cluster
definition:lines so each entry's first phrase uniquely cues its semantic role on reverse-recall flashcards. Previously the five entries clustered loosely around English glosses like "Want to / Plan to / Intending to / In order to / So that," which feel close to a learner. New cues each anchor to the unique trait that distinguishes the form:
The body prose of all five entries is unchanged — already audited in earlier batches. This PR is scoped to the definition: field.
[0.5.13.0] - 2026-05-03 — Grammar disambiguation pass: 'because' & 'but' families
Added
- ~(으)니까 — net-new entry sourced to Y&B §7.1.6. Korea's everyday spoken 'because' was a real gap in the library. Covers both Y&B uses (causation as subjective reasoning, and the discovery 'when [I did X], [I found Y]' use), the critical structural rule that ~(으)니까 is the *only* causal connective allowed before commands/proposals/suggestions/invitations/requests, and the mirror rule that ~아/어서 owns politeness/thanks/apology slots. Also covers tense markers (~었으니까), the topic-marked variant ~(으)니깐, and the sentence-ender ~(으)니까요.
Changed
- Disambiguated the 'because' family
definition:lines so each entry's first words uniquely cue the form on reverse-recall flashcards. Previously every entry started with the bare word "Because" and they all collided on the front of the recall card. New cues: ~아/어서 → "matter-of-fact, common-sense cause and effect; required for thanks, apologies, excuses"; ~(으)니까 → "your own reasoning — most common spoken; required before commands and suggestions"; ~기 때문(에) → "formal, common in writing — 'the reason is...'"; ~기에 → "literary, written-Korean only; spoken Korean uses ~길래 instead"; ~(으)ㄴ/는 바람에 → "something unexpected that caused trouble — usually a bad outcome". - Disambiguated the 'but' family
definition:lines. ~지만 → "sharp, direct contrast — sharper than ~(으)ㄴ/는데"; ~(으)ㄴ/는데 → "Sets up background — 'X, and / so / but...' (softer than ~지만; the relationship is implied)". Each now has a unique first cue and the two cross-reference each other.
definition: field — the part users see on the front of reverse-recall flashcards and on the grammar list page.
[0.5.12.0] - 2026-05-03 — Grammar audit batch 3 (3 entries — closes original 21-entry list)
Changed
- ~(으)ㄹ/(으)ㄴ/는 줄 알다/모르다 rewritten against Y&B §8.2.31. Three uses now (was two): (1) with 알다, mistaken presumption — including the "knew but ignored" sub-use ("당근이 몸에 좋은 줄 알지만 먹기 싫어요") and the frozen idiom 그럴 줄 알았어; (2) with 모르다, things you only now realize; (3) present 알다 + prospective ~(으)ㄹ only — the "know how to" skill use. All four modifier shapes documented (prospective, past prospective, present dynamic, state/result) where the prior entry only had ~(으)ㄹ. Fixed an incorrect distinction in the original tip: it claimed ~(으)ㄹ 줄 알다 vs ~(으)ㄹ 수 있다 was "learned vs innate." Y&B's actual distinction is "knowing how to" vs "currently able to," with the canonical sprained-pianist counterexample.
pattern:(singular) replaced withpatterns:(plural) covering ~(으)ㄹ 줄, ~(으)ㄴ 줄, ~는 줄. - ~(으)ㄴ/는 셈이다 sourced to NIKL (Y&B doesn't give it a dedicated section). Disambiguates upfront the three constructions that share the bound noun 셈: ~(으)ㄴ/는 셈이다 (this entry — evaluative reframing), ~(으)ㄹ 셈이다 (intent — "어쩔 셈이야?"), and ~(으)ㄴ/는/(으)ㄹ 셈 치다 (hypothetical — "없는 셈 치세요"). Adds the alternate adverbial form ~(으)ㄴ/는 셈으로 and a modifier-selection table for action vs descriptive vs noun + 이다.
- ~(으)면 ~(으)ㄹ수록 retitled to the textbook canonical doubled form (per Y&B §7.5.9), with the bare ~(으)ㄹ수록 framed as the everyday abbreviation. Replaces the misleading "doubles the emphasis" tip with the actual mechanic: the same verb is repeated, first with ~(으)면, then with ~(으)ㄹ수록. Pulls out Y&B's two named set expressions (나이를 먹을수록, 시간이 갈수록) where dropping the first verb is the norm, plus the productive 갈수록 + noun construction ("as X goes on") that derives from 시간 dropping.
[0.5.11.1] - 2026-04-30 — Card flip regression fix
Fixed
- Front of review card no longer leaks through after flip. The
position: relativeadded to.review-cardin 0.5.11.0 created a stacking context that brokebackface-visibility: hiddenon the card faces, leaving the (mirrored) front layered above the back after the flip. Moved the card-type badge out of the flipping element entirely — it now lives as a sibling of.review-card-inner, anchored to.review-card-perspective. The 3D transform chain on the faces is back to its pre-0.5.11.0 state. Single static badge is visually correct since both faces show the same label.
[0.5.11.0] - 2026-04-30 — Card-type labels + new-card cap tiers
Added
- Card-type label on review cards. A small uppercase badge in the top-left corner of each card (front and back faces) tells you whether you're reviewing a Grammar, Vocab, or Example card. Reader-captured
vocab_recallcards bucket as Vocab. Bucketing is a pure function ofcardType, so any card with an unrecognized type renders no badge instead of guessing. - New-cards-per-session tier guidance in Settings. The "New cards per session" description on
/settingsnow lists three reference tiers — Beginner/Sustainable (10–20/day), Intermediate (30–50/day), Intensive (50–100+/day, 3–5 hours of study) — so users have a yardstick instead of just a bare number input.
[0.5.10.0] - 2026-04-29 — Grammar audit batch 2 (4 entries, Y&B-sourced)
Changed
- ~더라고요 rewritten against Y&B §4.3.1.3. Reframed as the polite form of the observed-past evidential -더-, with the "no soliloquy" / authority nuance from -라고. Adds the past-base distinction (가더라고요 "I saw her going" vs 갔더라고요 "I noticed she had already gone") and the two first-person exceptions: dreams/unconscious actions, and the *reversal* for verbs of feeling (where the subject must be the speaker, since you can't observe a third person's feelings from a remote position).
- ~거든요 rewritten against Y&B §9.2. Now covers three uses: (1) reason for the previous sentence, (2) source of information / own experience, (3) standalone with no preceding sentence — including the "rebuke the assumption" use ("난 어제 했거든. 오늘 네가 해."). Adds ~겠거든요 (future/inferential base). Replaces the misleading "~거든 without 요 is conditional in some dialects" tip with a correct note that the same ending has a separate connective use, distinguishable by clause position.
- ~도록 rewritten against Y&B §7.6.3. Adds the structural constraint (no tense markers before ~도록), Y&B's explicit "weaker causative force than ~게" comparison, and the major missing use: ~도록 하다 for orders ("make sure you…"), proposals ("let's make sure we…"), and promises ("I will make sure I…"). Reframes Use 2 around prolonged time (밤새도록, 늦도록, 해가 뜨도록) as the textbook anchor, with the hyperbolic 배가 터지도록 / 눈이 빠지도록 pattern as an extension.
- ~(으)려고 rewritten against Y&B §7.7.2. Expanded from 1.5 uses to 5: (1) mid-sentence purpose, (2) sentence-final fragment ("살을 빼려고?"), (3) sentence-final disbelief with 설마 ("설마 혼자서 삼겹살 10인분이나 먹으려고?"), (4) ~(으)려고 하다 split into intention / imminence / past-unfulfilled, (5) connective combinations (~려다가 abandoned intention, ~려나 봐요 future conjecture, ~려니까 future-grounded causation). Adds Y&B's explicit ~(으)러 vs ~(으)려고 contrast (motion verbs only vs any processive), the full set of contracted forms (~려고요, ~려 해요, ~렵니다), and the colloquial ~ㄹ려고 pronunciation note.
[0.5.9.0] - 2026-04-29 — Easy actually means easy + reader knows what's in your deck
Added
- FSRS short-term scheduling is now toggleable. Settings → Study has a new "Short-term scheduling" toggle, default OFF. When OFF, rating a card "Easy" jumps it straight to a long interval — the way most people expect. When ON (the FSRS-5 default), Easy still triggers short retention checks before graduating, so a card you marked Easy can reappear in minutes. The preference is sent with each review submission so server-side scheduling matches client-side.
- Reader popup now knows when a word is already in your deck. Tapping a word you've previously captured shows "Already in your deck — review it" (with a link to /review) instead of the Capture button. The capture endpoint was always idempotent on (user, vocab), so duplicate clicks never created duplicate cards, but the UI didn't surface that state. Now it does.
Changed
- Reworded the "new cards deferred" session message. It now reads "X cards held back to keep this session manageable — they'll appear as you graduate current ones. Adjust the cap in Settings → Study." Old wording implied they'd come back literally next session, but they actually surface as you graduate current new cards into the review queue (which usually takes several sessions, not one).
[0.5.8.0] - 2026-04-29 — Configurable new-card cap + counter words capturable
Added
- New cards per session is now configurable. Settings → Study has a number input where you can pick anywhere from 0 to 100 new cards per review session (was hardcoded to 20). Reviews of already-learned cards are still uncapped. Set to 0 for review-only mode — useful when you want to grind reviews without taking on new vocabulary.
- Inline "Saved" microconfirmations on settings. Both the Korean Text-to-Speech toggle and the new-cards-per-session input now flash a brief "Saved" label next to the control after autosaving, so you can tell the change actually took effect without a heavyweight toast.
Changed
- Bound nouns and counter words can now be captured as flashcards. Tapping 척, 마리, 명, 것, 수, 줄, and other bound nouns (의존명사 / Kiwi POS NNB) in the reader now produces a valid capture instead of "pos must be one of NNG, NNP, VV, VA, MAG (content POS)." NNB is added to both the reader's tappable-content allowlist and the capture endpoint's POS validation.
- Settings → "Korean pronunciation" renamed to "Korean Text-to-Speech." Clearer about what the toggle actually controls.
[0.5.7.1] - 2026-04-29 — Dashboard counters fix
Fixed
- Dashboard "Sessions" counter and "Next review" both work again. When you finished a review session, the end-of-session bookkeeping (creating a local backup, computing the next-due date) wasn't running because the conditional that gated it checked a ref value that hadn't been updated yet by React. Result: the dashboard's session counter sat at 0 and next-review showed "-" no matter how many sessions you completed. The check now uses a synchronously-computed flag, so finishing the last card reliably triggers the backup write and dashboard refresh.
[0.5.7.0] - 2026-04-28 — Korean Text-to-Speech
Added
- Flashcards auto-speak the Korean side. When you reveal a flashcard, the Korean content (whichever side it's on) plays automatically through your browser's built-in voice. Cards where the Korean is on the front speak as soon as they appear; cards where Korean is on the back speak the moment you flip them. No setup, no API keys — uses the Web Speech API and your device's installed Korean voice.
- Tap-to-hear in the reader. Tap any word in a reading passage and hear it pronounced immediately, in parallel with the lookup overlay loading. Especially useful for words you can read but aren't sure how to say.
- Settings page (
/settings). New page in the user nav with a toggle to turn pronunciation on or off. Default ON. Includes a "Preview voice" button so you can hear what the voice sounds like before deciding. Preference is stored per device in localStorage. Same chokepoint also handles the edge case where you disable TTS mid-utterance — the current word stops immediately rather than finishing. - Voice quality varies by platform: Chrome/Edge desktop and iOS sound natural; Linux and older Android can sound robotic. We accept that variance for now since the API is free, ships in the browser, and works offline.
[0.5.6.0] - 2026-04-28 — Grammar Audit, Batch 1 (Y&B sourcing for existing entries)
Changed
- 14 existing grammar entries audited and rewritten against Yeon & Brown. Continuing the textbook-fidelity work from v0.5.5.0, this pass goes back to the entries that pre-date the citation workflow and rewrites them to match Y&B's framing and capture the lexical/situational restrictions Y&B emphasizes. Entries updated:
~(으)ㄴ/는데(§7.3.12 — full sentence-final + suspensive coverage),~(으)ㄹ 때(§8.2.17 — adds ~때마다, ~적, noun+때),~(으)ㄹ 뻔하다(§8.2.25 — adds the 거의 다 restriction),~(으)면서(§7.3.7 — same-subject restriction + ~(으)면서도),~나/(으)ㄴ가 보-(§5.5 — first-person restriction + 싶어하- analogue),~(으)ㄹ 텐데(§9.12 — sentence-final "I'm afraid" + counterfactual regret),~기 때문(에)(§2.2.4.2 — three explicit restrictions: no commands, no thanks/apology, reverse-order events),~(으)ㄹ 생각이다(§8.2.5 — repositioned within Y&B's six-noun family),~(으)ㄴ/는 바람에(§8.2.23 — corrected three previous errors: adjectives allowed, ~(으)ㄴ form exists, positive outcomes possible),~아/어서(§7.1.1 — major expansion: dual causal/sequential, common-knowledge framing, special idioms),~고 싶-(§5.3.4 — 1st/2nd person rule + descriptive→processive 싶어하-),~지만(§7.2.1 — three uses + ~기는 하지만 + polite openers),~(으)ㄴ/는 적이/일이 있다/없다(§8.2.29 — added the habitual ~는 일이 있다 use missed entirely before),~(으)ㄴ/는 데다(가)(NIKL fallback — Y&B doesn't cover it). - NIKL Korean Basic Dictionary established as fallback authority. When Yeon & Brown doesn't dedicate a section to a pattern (as with ~(으)ㄴ/는 데다(가)), the entry is sourced to 국립국어원 한국어기초사전 (krdict.korean.go.kr) instead. The citation hierarchy is now: Y&B first; NIKL when Y&B is silent.
For contributors
- 7 entries remain in the audit queue:
~더라고요,~거든요,~도록,~(으)려고,~(으)ㄹ 줄 알다/모르다,~(으)ㄴ/는 셈이다,~(으)ㄹ수록. Future PRs.
[0.5.5.0] - 2026-04-27 — Grammar Library Expansion (Yeon & Brown sourced)
Added
- 25 new advanced grammar entries. The library more than doubled (20 → 45 entries), all written for upper-intermediate and advanced learners (TOPIK 5-6). Every new entry is sourced to a specific section of *Korean: A Comprehensive Grammar* (2nd ed.) by Jaehoon Yeon and Lucien Brown, with headline example sentences lifted verbatim from the textbook for direct attribution. New patterns include
~기 마련이다(it's only natural that),~기는커녕(let alone),~(으)ㄴ/는데도 (불구하고)(despite),~다시피(as you know),~(으)ㄹ 리(가) 없다(no way that),~(이)야말로(indeed),~(으)ㄹ까 봐(worried that, plus tentative own-intention),~기 십상이다(likely to, with negative bias),~(으)ㄹ 따름이다(humble apology/thanks),~기 위해(서) / ~기 위한 / 을/를 위해(서)(in order to),~(으)ㄴ 채(로)(in an unusual state),~았/었더라면(counterfactual past with regret),~기에(formal cause),~더니 / ~았/었더니(observed past with subject-person distinction),~(으)ㄹ/(으)ㄴ/는 모양이다(it seems),~(으)ㄹ 정도로(to the extent that),~기에 망정이지(fortunately, otherwise),~(으)ㄹ 만하다(worth doing),~다(가) 보면(iterative conditional),~게 되다(to come about / turn out, with apology and humble framings),~(으)ㄴ/는 김에(while you're at it),~자(마자)(as soon as),~던(continuous past modifier), and~기는 하다(concession with implied limitation). sourcefield on grammar entries. The grammar content collection schema gained an optionalsourcefield for citing the primary reference each entry was drafted from. The grammar detail page renders the citation as a muted italic footer below the Examples section, so readers can chase the entry back to the textbook section it came from.
For contributors
- The
sourcefield is free-form string. Format used for the new entries:Yeon & Brown, *Korean: A Comprehensive Grammar* (2nd ed.), §X.Y.Z. Existing entries (the original 20) don't carry citations and theirsourcefield is empty — they pre-date the textbook-grounded workflow.
[0.5.4.0] - 2026-04-26 — LLM Tag Leakage Hotfix
Fixed
- Captured vocab cards no longer show LLM protocol artifacts. When you tapped "Add to deck" on a Korean word, the back of the resulting flashcard sometimes ended with literal
andstrings — leaked closing tags that Haiku occasionally emits inside its tool-use response when it mixes up the JSON tool-use format with older XML-style function-calling formats it was trained on. The reader's lookup endpoint now strips XML-shaped tags from every Haiku-produced field (lemma, part of speech, gloss, example sentence) on the way into the cache AND on the way out, so old dirty cache entries are also cleaned up on the next read. A separate Postgres cleanup script (scripts/cleanup-llm-tag-leakage.sql) repairs any rows already in the database. Future surprises (a different leaked tag name) are caught automatically because the sanitizer now strips ANYshape rather than maintaining a name allowlist that lags Haiku's quirks.
[0.5.3.0] - 2026-04-25 — Mobile Polish
Fixed
- Mobile navigation no longer squishes on phones. The top bar collapsed all four nav items, the email, the logout link, and the theme toggle into a single horizontal row that overflowed on iPhone-sized screens. Below the desktop breakpoint, the nav links now collapse into a hamburger menu that slides down beneath the header — tap once to open, tap a link or anywhere outside to close, page navigation auto-closes the menu. Logo and theme toggle stay visible in the header for one-tap access.
- Reading a grammar page from inside a passage now lets you return to the passage. Tapping a grammar pattern from the reader (either the inline word with the underline or the right-rail index) used to land you on the grammar detail page with only a "Back to Grammar" link, throwing you out of the passage you were reading. The reader now passes the passage ID through the link, and the grammar page renders an additional "← Back to your passage" link that drops you exactly where you were. The original "Back to Grammar" link stays in place for direct visits.
[0.5.2.0] - 2026-04-24 — Reader Hardening
Added
- Full grammar overlay coverage. Every one of the twenty grammar points on the site now renders with the morpheme-bold-and-superscript treatment in the reader.
~나 보다 / ~(으)ㄴ가 보다was the last holdout; it ships with a single pattern that handles all four surface forms (먹나 보다,아픈가 보다,좋은가 보다,학생인가 보다) because Kiwi tags both나andᆫ가/은가as EC endings in the auxiliary-verb context. - Alternative-pattern schema primitive. Grammar MDX files can now carry a
patterns(plural) field listing multiple alternative morpheme sequences — useful for future grammars whose surface forms genuinely have different POS tag shapes (not just different allomorphs). Existing grammars usingpattern(singular) keep working unchanged.
Security
- POS allowlist on capture. The reader's capture endpoint now rejects any
posvalue outside the seven Kiwi content tags (NNG, NNP, VV, VV-I, VA, VA-I, MAG). Matches the in-reader coloring set. Prevents a caller from polluting the global vocab table with particles, endings, or other non-content morphemes. - Daily lookup rate limit. Cache misses on
POST /api/reader/lookupare now capped at 500 per user per UTC day. Cached lookups stay unlimited — repeat taps on words you've already looked up never cost anything. Closes an abuse vector where a stolen session could script lookups on novel surface forms to burn the Haiku budget; worst case is now \$0.50/day/session.
[0.5.1.0] - 2026-04-24 — Reader Polish
Changed
- Grammar overlay redesign. The in-text grammar hint changed from a dotted celadon underline (easily mistaken for a hyperlink) to a bold morpheme with a tiny superscript number keyed to a new right-rail aside. Each unique pattern gets one numbered row in the aside; hovering a row softly highlights every instance of that pattern in the text; clicking a row opens the grammar detail page. Reading a passage now comes with a glanceable index of what grammar is in it.
- Informative lookup popover. Tapping a word with a matched grammar pattern now shows the pattern's canonical name and English definition (e.g.
~지만 — But; however) inline, instead of a generic "Studied grammar" label that was misleading for patterns you hadn't added to your study list. - Plain-English parts of speech. The lookup popover now shows
noun,verb,adjective,adverb,proper nouninstead of the raw Kiwi POS codes (NNG,VV,VA,MAG,NNP). Irregular stems with-Isuffixes collapse to the same labels. - Captured words flip styling instantly. After you capture a word as a flashcard, it switches to the learning-underline treatment immediately. No more waiting for a page reload to see your work recognised.
Fixed
- Grammar overlays actually render. The overlap test in the reader's word renderer was inverted: it required the word to be inside the grammar match instead of the other way around, so no word ever got the overlay class despite the matcher producing valid matches. Fixed to a proper interval-overlap check, which also correctly highlights multi-word patterns like
~고 싶다that span two words. - Review card no longer shows mirrored Korean. During the 3D flip animation, the front face's Korean text was bleeding through the back face because
backface-visibility: hiddenis unreliable over translucent surfaces. Both review-card faces now paint on an opaque background, so you see one card at a time — no ghostly mirrored duplicate. - Word-lookup popover no longer goes translucent on hover. The popover was inheriting a hover rule from the generic card surface that dropped its background to a 4%-alpha tint during mouseover. It now stays solid regardless of cursor position.
Infrastructure
- Migrated Railway deploys from Nixpacks to Railpack. Railway deprecated Nixpacks and silently ignores
nixpacks.tomlin favor of the newrailpack.json. The build now downloads the 104MB Kiwi morphological model during the deploy step and persists it in the production image so tokenization works at runtime. - E2E tests now wait on real hydration. Three tests (auth gate, password show/hide, export success) were flaky because they clicked Preact islands before
client:loadhydration completed in dev mode. A newwaitForHydration()helper polls for thessrattribute to disappear fromastro-islandelements, which is the definitive signal that click handlers are attached. 41/41 E2E tests green.
[0.5.0.0] - 2026-04-23 — Reader
Added
- Haechi Reader. A new
/readersurface for bring-your-own Korean text. Paste a passage or upload a.txtfile, and the text renders as tappable, morphologically-segmented tokens powered by the Kiwi Korean tokenizer. Tap any word to see its dictionary form, part of speech, English gloss, and an example sentence — then capture it as a spaced-repetition flashcard with one click. - Smart word coloring by mastery. The reader computes FSRS retrievability client-side against your current vocab cards: words you've internalised fade to 55% opacity (you recognise them without thinking), words you're still learning get a subtle underline (you studied this, keep an eye on it), and unknown words stay in full weight. Your first-read heatmap tells you what matters without breaking the sentence.
- Grammar instance overlay. Nineteen of the twenty grammar points on the site now carry a morpheme-sequence pattern (
~지만,~(으)ㄴ 적이 있다,~(으)ㄹ 때, etc.). When these patterns appear in a passage, the reader underlines them in celadon and the lookup popover offers a link to the grammar page — so every page of Korean you read doubles as a review of what you've studied. - Passage persistence with resume. Create up to 20 passages a day (15,000 character cap each). Scroll position saves in the background; reopen a passage the next week and it scrolls to the word you left off on. Delete buttons are one-tap, captured cards survive deletion.
- Global lemma cache. Dictionary lookups are cached across all users keyed by surface form, so the second user to tap 먹었어요 never waits for Haiku.
- 32 new tests + full Haiku integration coverage. Matcher unit tests, Kiwi-validated pattern tests against every authored grammar, endpoint tests for all four reader routes, and an end-to-end reader happy-path test covering create → tap → lookup → capture.
Changed
- Card schema refactor. Cards now carry either a
grammarSlug(grammar-derived card) or avocabId(captured vocab card), enforced by a database XOR check constraint. Existing grammar cards are untouched; vocab cards are new territory for the reader. - Repaired E2E suite post-Living-Hangul. Existing tests that expected pre-redesign copy (
Korean Grammar) or that clicked the Study button in a Preact hydration race now pass reliably — 41/41 green.
Security
- Lookup cache poisoning hardening. Haiku output is clamped server-side before it hits the shared cache, and the sentence-context field is sanitised against XML-tag injection so a crafted passage cannot taint lookups for other users.
[0.4.0.0] - 2026-04-10 — Living Hangul
Changed
- Full UI redesign: "Living Hangul." The app now opens to a warm, scholarly aesthetic with a light off-white background, replacing the previous dark developer-chrome look.
- Typography overhaul. Noto Serif KR (Korean serif) replaces Geist Mono as the display font. Grammar titles rendered as typographic heroes at 28-56px. Pretendard stays for body text.
- Korean accent colors. Persimmon (#C35831) and celadon (#7BA087) replace the blue/purple dual accent system. Historically Korean, not generic tech colors.
- Grammar browse is now a specimen gallery. Card grid layout with watermark characters (first Hangul at 4% opacity), accent line hover animation, and featured cards spanning two columns.
- Review session card flip. CSS 3D
rotateYanimation (300ms) replaces the opacity fade reveal. Both card faces always in DOM. - Grammar detail pages. Calligraphy brush stroke SVG accents beneath titles, watermark characters, uppercase section labels, vocabulary in card surfaces, examples with celadon left borders.
- Astro View Transitions. Grammar card titles morph into detail page headers. Cross-fade fallback for non-Chromium browsers.
- Dark mode: warm charcoal (#1a1917) instead of cold near-black (#0c0d12).
- Border radius increased from 6px to 8px across all components.
- Logo changed to
해치.in Korean with persimmon dot accent. - Active nav indicator changed from wavy underline to a clean 2px persimmon bottom line.
- Theme toggle now uses event delegation and
astro:after-swapfor reliable behavior across Astro View Transition navigations.
[0.3.0.0] - 2026-04-02 — AI Writing Tutor
Added
- AI Writing Tutor page (
/tutor). Write Korean essays, click Analyze, get grammar suggestions (via Claude Haiku) framed as questions, plus blind spot detection that cross-references your FSRS study history with grammar used in your writing. - Two tutor modes. Basic mode (no grammar studied yet) analyzes your writing against all available grammar and suggests what to study next. Full mode adds blind spot detection once you've started studying.
- Blind spot algorithm. Identifies grammar you've studied in SRS but avoided using in your essay. Ranked by fail count, review recency, and historical writing usage.
- Tabbed analysis results. Suggestions, Blind Spots, and Study tabs with count badges. Cleaner than a long scroll.
- Draft management. Create, save, switch between, and delete writing drafts with auto-save.
- Grammar usage heatmap. Visualizes which grammar points you use across essays (shown after 3+ analyses).
- Pulsing analyze button. Glowing box-shadow animation while Claude is working, so you know the analysis is in progress.
- 18 intermediate Korean grammar entries. ~는데, ~거든요, ~더라고요, ~(으)ㄹ 텐데, ~는 바람에, ~기 때문에, ~(으)ㄹ수록, ~고 싶다, ~(으)ㄹ 생각이다, ~지만, ~(으)ㄹ 때, ~도록, ~(으)ㄴ 적이 있다, ~(으)려고, ~(으)ㄹ 줄 알다/모르다, ~나 보다, ~(으)ㄴ/는 셈이다, ~(으)ㄹ 뻔하다.
requireAuth()API helper for DRY auth checks across new endpoints.- Database tables:
writing_draftandwriting_analysiswith Drizzle migration.
[0.2.4.1] - 2026-04-02 — Safe Production Migrations
Changed
- Schema migrations run automatically on deploy. Railway start command now runs
drizzle-kit migratebefore starting the app. Migrations are committed SQL files, reviewed before merge. No more--forceflag in production. - Removed CI migrate job. Railway handles migrations at deploy time, so the GitHub Actions schema push step is no longer needed.
Fixed
- SSL connection error on Railway. Railway's internal PostgreSQL uses self-signed certificates. The app now lets the connection string control SSL instead of forcing it, fixing
SELF_SIGNED_CERT_IN_CHAINerrors on signup/login.
[0.2.4.0] - 2026-04-02 — Railway + PostgreSQL Migration
Changed
- Database: Turso (SQLite) → PostgreSQL. All server-side data now stored in PostgreSQL via Drizzle ORM with
node-postgres. Schema usespgTable()with properbigintfor epoch timestamps,realfor FSRS floats. - Hosting: Vercel (serverless) → Railway (containers). Single
@astrojs/nodeadapter, no conditional adapter logic. - Auth adapter:
provider: 'sqlite'→provider: 'pg'. better-auth now uses the PostgreSQL Drizzle adapter. - E2E test infrastructure: Turso CLI → Docker PostgreSQL.
pg-ephemeral.shspins up a local PostgreSQL container for tests. - CI: removed Turso CLI install, removed Preview environment dependency. E2E tests use Docker directly. Schema push uses
drizzle-kit push --force.
Removed
@astrojs/dband@astrojs/vercelpackagesvercel.jsonanddb/config.ts(AstroDB schema)scripts/turso-ephemeral.sh
[0.2.3.0] - 2026-04-02 — DB Persistence Fix
Fixed
- Study data now persists to the server database. better-auth's session cookie is HttpOnly (invisible to JavaScript), so the app was always using local-only storage for logged-in users. Auth state is now injected server-side via
window.__AUTH__. - Dexie upgrade error on existing browsers. Users with v1 IndexedDB got "UpgradeError: Not yet support for changing primary key." The database now auto-recreates on upgrade failure since IndexedDB is just an offline cache.
- Card ID collisions between users. Card IDs now include the userId, so multiple users studying the same grammar get their own independent cards and FSRS state.
- Instant feedback when rating cards. Rating a card advances to the next card immediately. The server call runs in the background. No more 1.6-second wait between cards.
- Instant feedback when clicking Study. The button switches to "Studying" immediately. Card generation (previously 5.5 seconds) runs in the background.
- Card generation is 4x faster. Batch insert sends all cards in one database call instead of one per card.
- Silent API failures now logged. Card generation and review submission log warnings to the console when the server call fails, instead of silently falling back to local storage.
[0.2.2.0] - 2026-04-01 — Grammar Search & Pagination
Added
- Server-side search for grammar points. Type in the search bar, results filter with a 300ms debounce. Searches title and definition.
- Level filters always visible (previously hidden below 5 entries). Filter by beginner, intermediate, or advanced.
- Pagination with prev/next controls. 20 entries per page, ready for when the collection grows past a single page.
- API endpoint at
/api/grammar/listwith search, level filter, and pagination query params. Validates input, returns 400 on bad params. - URL state for search, filter, and page. Bookmark a filtered view, share it, reload it.
Changed
- Grammar list now fetches from the API instead of receiving all entries as props. Server renders the first page for instant SEO, client takes over after hydration.
[0.2.1.0] - 2026-04-01 — Footer & Changelog
Added
- Footer with grammar point count, copyright, and version badge linking to the changelog.
- Changelog page at
/changelogrendering CHANGELOG.md. Public, no login required. - Larger review card text for answer readability.
Changed
- Project documentation updated for v0.2.0.0: CLAUDE.md tech stack, DESIGN.md form tokens, TODOS.md unblocked items.
[0.2.0.0] - 2026-03-31 — Auth + Database Integration
User accounts and server-side data persistence. Sign up with email and password, study across devices, and keep studying offline on the subway.
Added
- User authentication with better-auth. Email/password signup and login. 30-day sessions.
- AstroDB (Turso/libSQL) server database with full schema: cards, review logs, review stats, plus better-auth managed tables.
- 5 API routes: card generation, due cards, review submission (FSRS server-side), sync, and health check.
- DataProvider abstraction with 3 implementations: LocalDataProvider (IndexedDB), ServerDataProvider (fetch API), SyncingDataProvider (offline-first with background sync).
- Offline-first sync: study offline, reviews sync automatically when you reconnect. Last-write-wins with UUID dedup for review logs.
- Login and signup pages with dark-first editorial design matching the existing app aesthetic.
- Auth gate on Study button: guests see an inline signup prompt instead of a redirect.
- User menu in header: email + logout when logged in, "Log in" link when not.
- Sync-then-logout flow: pending reviews sync before local data is cleared.
- Toast notification container for sync status feedback.
- Shared FSRS config (
fsrs-config.ts) ensures identical scheduling on client and server. - FSRS state snapshots in review logs (
state_before,state_after) for future conflict resolution.
Changed
- Astro 6 upgrade from Astro 5. Hybrid mode is now the default (no
output: 'hybrid'needed). - Date → number migration: all timestamps use unix milliseconds instead of Date objects. Dexie database auto-migrates.
grammarIdrenamed togrammarSlugacross the entire codebase for clarity.- All pages server-rendered: homepage, grammar pages, review, 404, login, signup. Enables auth-aware header.
- Components use DataProvider instead of direct Dexie imports. ReviewSession, StudyButton, DashboardStats, ExportImport all abstracted.
[0.1.1.0] - 2026-03-31 — Wider Layout + Grammar Search
The homepage and review pages now use the full 960px width. Grammar list gets search, filtering, and study status at a glance.
Added
- Grammar search and level filters on the homepage. Search by Korean title or English definition. Filter pills for beginner/intermediate/advanced (visible when 5+ grammar points exist).
- Study status badges on grammar list rows. See which grammar points you're studying or have paused, right from the homepage.
- Wider layout (960px) for homepage and review pages. Grammar detail pages stay at the comfortable 768px reading column.
- Consistent dashboard in all states. New users, active studiers, and "all caught up" users all see the 4-stat grid and review button. No more collapsing to a tiny welcome card.
- Playwright browser caching in CI. Saves ~25s on E2E test runs by caching the 280MB Chromium download.
Fixed
- Review page width now matches the homepage. Progress bar and counter span the full 960px, card stays centered at 480px.
- Main element flex shrink that caused narrow pages when content was small (flexbox child without explicit width).
- E2E test reliability in CI: hydration timing, DOM detachment handling, download event races.
[0.1.0.0] - 2026-03-31 — Visual Unification
The app now looks like it belongs to the same person who built the blog. Dark-first design, dual fonts, dashboard homepage.
Added
- Dark mode as the default theme, with light mode toggle. Persists to localStorage, respects system preference, no flash of wrong theme on load.
- Dashboard homepage with live study stats: cards due, time estimate, session count, next review time. Warm onboarding message for new users. "Start Review" button in review accent color.
- Geist Mono for UI chrome (navigation, labels, buttons, stats counters) alongside Pretendard Variable for Korean content. Two identities in one system.
- Two accent colors: blue (#7eb8f0) for grammar browsing, purple (#c4a7e7) for review sessions. Review page uses CSS variable scoping for automatic accent swap.
- Tailwind CSS 4 via @tailwindcss/vite plugin, replacing 450 lines of vanilla CSS with a design token system matching the blog.
- Translucent surface system with rgba() backgrounds and borders for cards and panels.
- Skip link for keyboard accessibility.
- Theme toggle with sun/moon icons in the navigation header.
Changed
- DESIGN.md rewritten to document the new design system: dark-first editorial aesthetic, dual-font typography, translucent surfaces, responsive specs, accessibility requirements.
- Grammar list rows now use Tailwind utility classes with hover states.
- Review card styling updated with new surface system and review-mode purple accent.
- Navigation shows active state with wavy underline on both homepage and grammar subpages.
- 404 page restyled to match new design system.
Fixed
- Grammar nav active state now highlights on grammar subpages (was only active on exact
/path). - Theme-color meta tag set correctly on initial load (was empty string).
- Missing alert-link and alert-dismiss CSS classes restored from pre-migration styles.