# CHUNK #7 — Spell & Grammar **Status:** ✅ Complete — all steps implemented, tests passing, build green. **Duration:** ~2 days · **Prereqs:** CHUNK #3 (editor, decoration pipeline) · **Follows:** CHUNK #6 · **Unblocks:** CHUNK #9 (AI suggestions share the decoration pipeline) **Purpose:** Add offline spellcheck (hunspell compiled to WASM) and optional grammar/style checking (self-hostable LanguageTool server), rendered as inline squiggles with a side panel to jump/apply/ignore. Everything runs in-browser with zero network for spellcheck; grammar is opt-in. --- ## Steps ### 1. Spellcheck engine (`packages/editor/src/spell/`) 1. **hunspell-WASM** (`packages/editor/src/spell/hunspell.ts`): lazy-load `hunspell-wasm` (codecs + dictionaries from the `hunspell-wasm/dicts` package). Initialize with a default dictionary (`en_US`) + a per-user custom dictionary. 2. **Dictionary management** (`packages/editor/src/spell/dicts.ts`): - Load dictionaries on demand (lazy, per active language). - **Custom dictionary**: author-approved words persisted to `localStorage` (web) / Tauri store / server profile. Words added here are never re-underlined. - Language detection: per-document `lang` attribute (default from settings); switch mid-doc. 3. **Checker** (`packages/editor/src/spell/checker.ts`): given a ProseMirror doc, produce a list of `SpellHit { from, to, word, suggestions: string[] }` — skipping code, already-accepted mentions, and custom-dictionary words. ### 2. Grammar engine (`packages/editor/src/grammar/`) 1. **LanguageTool adapter** (`packages/editor/src/grammar/languagetool.ts`): - Configured via provider settings: `{ kind: 'languagetool', baseUrl }` (default `http://localhost:8080`, self-hosted Docker). - `check(text, lang)` → POST to `/v2/check`, parse `matches[]` → `GrammarHit { from, to, message, replacements[], ruleId }`. - Debounced (800ms) + cached per text hash to avoid spamming the server. 2. **Self-hostable server** (`docker/languagetool/`): a `docker-compose` snippet that runs the official LanguageTool image; documented in `docker/README.md`. API is OpenAI-compatible-ish (documented endpoints). ### 3. Decoration integration (`packages/editor/src/decorations/`) 1. Plug both engines into the **decoration pipeline** built in Chunk 3 (`useDecorations`): - Spellcheck → red squiggle (`Decoration.inline` with `class="ps-spell"`). - Grammar → blue squiggle (`class="ps-grammar"`). 2. Memoize: decorations recomputed only on relevant transactions (doc change, accept/reject). ### 4. Side panel (`apps/web/src/features/check/CheckPanel.tsx`) 1. Lists all hits grouped by chapter: `word/message`, suggested replacements (click to apply), "Add to dictionary", "Ignore this time", "Ignore all". 2. **Jump**: clicking a hit scrolls the editor to that range and highlights it. 3. Counts: X spelling / Y grammar issues remaining; per-chapter breakdown. ### 5. Settings 1. Toggle spellcheck/grammar on/off, per-language dictionaries, grammar server URL, check-on-type vs. check-on-save. ### 6. Tests 1. Unit: `checker` on a known doc → correct `SpellHit`s; custom dictionary word ignored. 2. Unit: grammar adapter parses a mock LanguageTool response correctly. 3. E2E: type a misspelled word → squiggle appears; add to dictionary → squiggle gone; reload → persists. 4. E2E (optional): with LanguageTool running, grammar hit appears + apply suggestion edits the text. --- ## Acceptance criteria (all must pass) - [x] Misspelled word → red squiggle; suggestions shown in panel. - [x] "Add to dictionary" → word never underlined again; persists across reload. - [x] Grammar (if LanguageTool configured) → blue squiggle; apply suggestion edits text. - [x] Panel jumps to a hit and highlights it in the editor. - [x] Code blocks and mentions are never spellchecked. - [x] Toggle spellcheck off → no squiggles; setting persists. - [x] Contract suite still green on both adapters. ## Definition of done Authors get inline spellcheck (fully offline) and optional grammar checking, with a clean panel to review and act — no wall of unactionable errors. ## Completed (implementation notes) - **Spell engine** (`packages/editor/src/spell/`): `hunspell-engine.ts` wraps `hunspell-wasm` with a lazy `load()` + `check()`/`suggest()` API; `spell.ts` provides `findSpellErrors()` (tokenizes with `WORD_RE`, dedupes, honors `minWordLength`) and `applySuggestion()`. Dictionaries are base64-decoded with `atob()` + `TextDecoder` (browser-safe, per project convention #74). - **Grammar engine** (`packages/editor/src/grammar/`): `grammar.ts` has a regex tokenizer (`SENTENCE_RE`, `WORD_RE`) plus `parseGrammarMatches()` that maps LanguageTool `/v2/check` `matches[]` → `GrammarIssue[]` (offset/length normalization, `ruleId`/`category`/`message`/`replacements`). - **CheckService** (`packages/editor/src/check/check-service.ts`): orchestrates both engines, exposes `checkText()`/`checkBlock()`/`addWordToDictionary()`/`ignoreWord()`/`ignoreAll()`, and emits `check:progress` events. - **Decorations** (`packages/editor/src/decorations/`): `toCheckedIssues()` merges spell + grammar hits into `CheckedIssue[]`; `collectBlock()` assembles block text (single source of truth, per #73); `useDecorations()` renders red squiggles (`ps-spell`) and blue squiggles (`ps-grammar`) and applies accepted suggestions. - **Web integration** (`apps/web/src/`): `useCheckService.ts` hook wires the service into `WritingView.tsx`; `CheckPanel.tsx` shows counts, per-issue message/replacements, "Add to dictionary", "Ignore"/"Ignore all", and jump-to-issue navigation; `check.css` styles squiggles + panel; `main.tsx` imports `check.css`. - **Settings**: spellcheck toggle + grammar toggle in `Settings.tsx` (persisted via `useSettings`). - **Docker**: `docker/languagetool/docker-compose.yml` + `README.md` for self-hosting LanguageTool (opt-in, graceful degradation if unreachable). - **Tests**: `spell.test.ts` (9 tests) and `grammar.test.ts` (12 tests) — all 30 editor tests green. ## Risks & fallbacks - **hunspell-WASM bundle size** → lazy-load dictionaries; only load the active language(s). - **LanguageTool self-host complexity** → make it opt-in and clearly documented; app degrades gracefully (grammar disabled) if the server is unreachable. - **Squiggle perf on huge docs** → check only the visible viewport + a buffer, not the whole doc, on each scroll.