Skip to content
해치.
← Back

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

[0.8.2.0] - 2026-05-27 — Vocab cards remember where you met the word

Added

Changed

Fixed

[0.8.1.0] - 2026-05-19 — Speak: in-app shadowing (Reader mic + scoring)

Added

Fixed

For contributors

[0.8.0.0] - 2026-05-13 — Speak: Korean shadowing service + CLI (v0)

Added

Internal

[0.7.11.0] - 2026-05-13 — Grammar content audit (PR2): tighter definitions, no Korean leakage on recall cards

Changed

Internal

[0.7.10.0] - 2026-05-13 — Grammar content audit: lazy Y&B attribution stripped from 60 entries

Changed

Internal

[0.7.9.0] - 2026-05-12 — Dark mode audit + screenshot tooling + perf measurement

Fixed

Added

Internal

[0.7.8.2] - 2026-05-11 — Review card front + back now route through Newsreader

Fixed

[0.7.8.1] - 2026-05-11 — Review session typography + content-aware Hangul hero

Changed

[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

Changed

Internal

[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

Internal

Not yet rolled out

[0.7.6.2] - 2026-05-11 — Back link in Fraunces small-caps + hero vertical rhythm

Changed

[0.7.6.1] - 2026-05-11 — TOC rail in Fraunces small-caps

Changed

[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

Changed

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.html added 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 the Fraunces Variable family — font-style: normal yields upright glyphs, italic yields 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-display CSS variable is now used for both italic *and* upright Fraunces with font-style toggling 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. entryNumber and level remain 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 vocab frontmatter, 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 → ~form only — the Korean form is the stable identifier.

Internal

  • src/components/grammar/EyebrowRow.astro deleted (no callers).
  • e2e/grammar.spec.ts Vocabulary 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 NN now 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 the definition field — 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.
A full audit of the other 61 entries for the same Korean-in-definition pattern is queued in TODOS.md.

[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: smooth setting. 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 Grammar on 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 uses field on grammar entries. New uses: [{ 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 seeAlso field on grammar entries. New seeAlso: ["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 at src/lib/voice-line.ts — splits an entry's definition field 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 at src/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 seeAlso integrity check. scripts/validate-content.ts now fails the build if any seeAlso slug doesn't resolve to another entry, or if an entry references itself. Matches the existing entryNumber collision check and runs on every bun 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 (iterates uses[] then continues with the MDX body) → Vocabulary → Examples (3-line) → See also → Sources (or legacy source footer 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 structured uses[] 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's

    cleanly.

  • Heading hierarchy on entry pages unified — Use & Meaning, Vocabulary, Examples, See also, and Sources all render as

    (sibling section headings under the entry's

    Hangul 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-foreground instead of the persimmon accent, leaving the Study button and FSRS Good state as the page's only persimmon chrome positions.

Fixed

  • scripts/validate-content.ts seeAlso 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-label now uses font-style: italic to match the rest of the eyebrow stack. The previous font-style: normal paired with the italic-axis Fraunces font produced synthetic-upright Fraunces, which DESIGN.md's typography section warns against.
  • SourcesBlock comma separators are now aria-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 entryNumber on 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 sources array. New sources: [{ 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 legacy source (singular, free-form string) kept for backward compatibility on entries that haven't been re-cited yet.
  • gloss field 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 unique entryNumber and 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 duplicate entryNumber conflicts 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 via unicode-range).
  • Fraunces Variable (@fontsource-variable/fraunces, italic axis only via wght-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-foreground token (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-mono and font-italic-display Tailwind utilities — wired up via @theme inline so 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-foreground row 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

  • /dashboard page. Personal study stats (cards due, minutes, sessions, next review) live here now. Login redirects to /dashboard by default. Direct visit redirects to /login if 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:full now disables email features for the test run (RESEND_API_KEY=""), unblocking session-based tests that broke when v0.7.0.0 added requireEmailVerification. 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-link to .logout-link class 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. Includes autoSignInAfterVerification so first-time users land authenticated after verifying.
  • Password reset flow. New /forgot-password page (request a reset link by email) and /reset-password page (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-email landing 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.ts wraps the Resend SDK with a single sendEmail(...) 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 to onboarding@resend.dev if unset (Resend's testing sender; deliverability is poor — verify a domain in production).
Graceful degradation when email isn't configured. All email-dependent features are gated on RESEND_API_KEY presence. When the key is unset:
  • requireEmailVerification defaults to false so basic signup still works (users can log in immediately, no verification needed).
  • The magicLink plugin is not loaded.
  • sendResetPassword is undefined.
  • The login form hides the magic-link toggle and forgot-password link.
This means the PR is safe to merge before 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.ts adds the magicLinkClient plugin so the form can call authClient.signIn.magicLink(...).
  • src/components/AuthForm.tsx rewritten to support three flows on one form: signup with confirm-password, password login, magic-link login (toggleable on the login screen).
  • src/layouts/Base.astro exposes window.__AUTH__.emailEnabled (computed from server-side RESEND_API_KEY presence) 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 — now when 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.reviewedAt clusters separated by gaps of more than 30 minutes, via a new GET /api/stats/sessions endpoint. Reflects activity across devices and isn't capped.

Added

  • src/lib/sessions.ts with countSessions() — 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/sessions endpoint (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 used NNB (matching Y&B's analysis of 동안 as a bound noun), but Kiwi tokenizer tags 동안 as NNG (general noun). The pattern wasn't actually matching real text. Now NNG, 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:
- ~(으)ㄹ 때 → "When / at the time of (the basic 'at the moment of' — 학생일 때 'when I was a student'; past form is ~았/었을 때)" - ~자(마자) → "As soon as / the instant after (with 마자) — second clause follows immediately, often unexpectedly; without 마자 it loosens to 'when / upon'" - ~더니 / ~았/었더니 → "I noticed/saw that X, then Y — observed events with consequence. ~더니 for he/she/you (no past); ~았/었더니 for I/we (with past)" - ~(으)면서 → "While / at the same time (one subject doing two things at once — for two different subjects use ~는 동안에 instead)" - ~다(가) / ~았/었다(가) → "While doing / in the course of (no tense) — first action interrupted; or 'did X, then' (with past ~았/었) — first action completed, then a (often unexpected) follow-up" - ~는 동안(에) / ~는 사이(에) → "While / during (two different subjects acting at the same time — contrast with ~(으)면서 for same-subject simultaneity)"

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:
- ~것 같다 → "I think / it seems (most common spoken hedge — also softens definite statements you're sure of: 감기 걸린 것 같아요 = 'I have a cold,' said politely)" - ~나 보- / ~(으)ㄴ가 보- → "It seems / looks like (inferring about someone or something else from sensory evidence — doesn't work with 'I' subjects)" - ~(으)ㄹ/(으)ㄴ/는 모양이다 → "It seems / looks like (more formal — judging from the circumstances; 모양 = 'shape/form'; reach for ~것 같다 for everyday spoken use)"

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:
- ~고 싶- → "Want to (1st/2nd person desire only — 'I want to' / 'do you want to?'; for he/she swap to ~고 싶어하-)" - ~(으)ㄹ 생각이다 → "Thinking of / planning to (a mental plan — 생각 means 'thought'; not yet a firm commitment)" - ~(으)려고 → "Intending to (mid-sentence — same person does both clauses) / about to / (with 설마, sentence-final) could it really?" - ~기 위해(서) → "For the sake of / in order to (formal purpose; can attach to a noun directly as N을/를 위해서)" - ~도록 → "So that (often a different subject achieves the result) / until (extended time — 밤새도록) / (with 하다) make sure to"

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.
The body prose of all six existing entries is unchanged (already audited in earlier batches). This PR is scoped to the 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 with patterns: (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.
This closes the original 21-entry audit list (batches 1–3): batch 1 v0.5.6.0 = 14 entries, batch 2 v0.5.10.0 = 4 entries, batch 3 = 3 entries.

[0.5.11.1] - 2026-04-30 — Card flip regression fix

Fixed

  • Front of review card no longer leaks through after flip. The position: relative added to .review-card in 0.5.11.0 created a stacking context that broke backface-visibility: hidden on 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_recall cards bucket as Vocab. Bucketing is a pure function of cardType, 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 /settings now 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).
  • source field on grammar entries. The grammar content collection schema gained an optional source field 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 source field 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 their source field 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 and strings — 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 ANY shape 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 using pattern (singular) keep working unchanged.

Security

  • POS allowlist on capture. The reader's capture endpoint now rejects any pos value 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/lookup are 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 noun instead of the raw Kiwi POS codes (NNG, VV, VA, MAG, NNP). Irregular stems with -I suffixes 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: hidden is 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.toml in favor of the new railpack.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:load hydration completed in dev mode. A new waitForHydration() helper polls for the ssr attribute to disappear from astro-island elements, 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 /reader surface for bring-your-own Korean text. Paste a passage or upload a .txt file, 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 a vocabId (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 rotateY animation (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-swap for 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_draft and writing_analysis with 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 migrate before starting the app. Migrations are committed SQL files, reviewed before merge. No more --force flag 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_CHAIN errors 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 uses pgTable() with proper bigint for epoch timestamps, real for FSRS floats.
  • Hosting: Vercel (serverless) → Railway (containers). Single @astrojs/node adapter, 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.sh spins 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/db and @astrojs/vercel packages
  • vercel.json and db/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/list with 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 /changelog rendering 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.
  • grammarId renamed to grammarSlug across 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.

62 grammar points
Copyright © 2026 | All rights reserved v0.8.2.1