# Inkhaven 1.2.9 — Prose-craft tools for novelists

1.2.9 turns inkhaven from a *book editor* into a
*book editor that audits your prose while you write
it*.  Nine new features, all keyed to the same
writing-craft idea: catch the mechanical weaknesses
(drone, telling, hedging, lexical ruts) inline,
while the words are still soft, instead of
discovering them at revision time.

The headline overlays (always-on, zero latency):

  * **Filter-word highlight** (`Ctrl+B Shift+F`) —
    intensifier crutches + hedges, stemmed,
    multilingual (English / Russian / French /
    German / Spanish).
  * **Repeated-phrase detector** — n-gram window
    flags writer-crutch gestures (`lifted her
    shoulders` matching `lifting her shoulders`)
    via Snowball stemming.
  * **Show-don't-tell flag** — copula + emotion-
    adjective 2-grams, manner-of-emotion adverbs,
    cognition verbs.

On-demand modals + chips:

  * **Concordance view** (`Ctrl+B Shift+L`) —
    project-wide lexical fingerprint with KWIC
    samples.
  * **Sentence-rhythm gauge** (`Ctrl+B Shift+H`) —
    measures the felt rhythm of the prose, mapped
    against Gary Provost's "this sentence has five
    words" parable.
  * **POV / character chip** (`Ctrl+B Shift+P`) —
    status-bar surfaces the heuristic POV character
    + supporting cast, driven by the existing
    characters lexicon.
  * **Writing-streak heatmap** (`Ctrl+B Shift+G`) —
    GitHub-style 91-day word-activity grid.
  * **Scene-break navigation** (`Ctrl+B <` /
    `Ctrl+B >`) — jump between typographic dividers
    inside the open paragraph.

Accessibility:

  * **Text-to-speech read-aloud** (`Ctrl+B S`) +
    **save as audio** (`Ctrl+B Shift+R`) — provides
    the *ear* feedback that catches what the eye
    glides over.

AI-augmented:

  * **AI show-don't-tell scan** (`Ctrl+B Shift+T`) —
    sends the open paragraph to the configured LLM
    with a system prompt asking for telling phrases
    plus concrete rewrites.

Each feature has its own detail section below.

## Earlier planning notes (1.2.9 cycle scratchpad)

### Split view — the deferred 1.3 work

1.3 has been "next release" since 1.2.5.  The reason it
keeps slipping is that each polish cycle finds enough
small items to fill the branch.  1.2.9 should be the
last polish cycle before this happens — once 1.3 lands,
the F4 split-edit workflow + a real two-pane editor +
side-by-side snapshot diff land together.

### Carryovers from 1.2.8

The 1.2.8 release notes listed three items that didn't
ship:

- **Cascade-impact AI on event re-time.** Optional
  `Ctrl+V Shift+I` extension that flags the linked-
  paragraph fallout when an event is moved.
- **F11-on-paragraph "suggest event" variant.** Propose
  a draft event from the prose under the cursor.
- **Visual editor overlay for `Ctrl+V h`.** Tab +
  trailing-whitespace glyphs without breaking the
  column-bookkeeping that match + lexicon highlights
  depend on.

### Documentation corrections

- **Rename "wiki-links" → "paragraph links" / "linked
  paragraphs" everywhere.**  The 1.2.6 / 1.2.7 docs and a
  handful of in-code comments call `Node.linked_paragraphs`
  "wiki-links", which is misleading: there is NO
  `[[target]]` inline syntax in paragraph bodies — no
  parser scans prose, no auto-conversion happens.  The
  linking system is purely metadata: an explicit array
  of target UUIDs maintained through chords (`Ctrl+V L`,
  `Ctrl+V Shift+L`) + Bund (`ink.event.link_paragraph`).
  Mechanical pass across roughly 7 files / 15 occurrences:
  KEYBINDING.md, story_view.rs comments, the Ctrl+B U
  undelete status string, the kill-ring restore notice,
  the keybind.rs help blocks for the link / backlink
  actions, RELEASE_NOTES/1.2.6.md and 1.2.7.md.  Should
  be one of the first commits in the cycle so later
  features can call them by the right name from day one.


### Real-project polish (rolling)

Bug reports from real users land here. Currently empty.


## What's landed so far in 1.2.9

### `inkhaven doctor --voices` lists installed TTS voices

Companion to the TTS feature.  Run from any directory
(no project context needed — this is a host-OS query):

```
$ inkhaven doctor --voices
inkhaven TTS voices — v1.2.9
================================================================
name               language  gender
Katya              ru-RU     female
Katya (Enhanced)   ru-RU     female
Milena             ru-RU     female
Milena (Enhanced)  ru-RU     female
Yuri               ru-RU     male
…
```

Sorted by language then name; suitable as a substring
reference when choosing `editor.tts.voice` in HJSON.
Falls back to a stderr message + non-zero exit on
engine-init failure (Linux without speech-dispatcher,
containers, etc.) so shell pipelines can branch on
availability.

### Show-don't-tell flag — inline overlay + AI scan

Two complementary layers for catching prose that
*tells* the reader an emotion instead of *showing*
it through behaviour, sensory detail, dialogue, or
action.

**Layer 1: regex-based inline overlay** (always-on,
zero latency, instant feedback while typing)

A new `ShowDontTellDetector` joins the filter-word
and repeated-phrase detectors under the
`Ctrl+B Shift+F` style-warnings toggle.  Tokens
matched by any of three patterns get a teal
underline:

  1. **Copula + emotion adjective** — a 2-gram
     `(linking_verb)(emotion_adjective)` where the
     verb is `be` / `is` / `are` / `was` / `were` /
     `seem` / `feel` / `appear` / `look` / `become`
     / `remain` / `grow` / `sound` (stemmed) and the
     adjective is one of ~70 curated emotion lemmas
     (`angry`, `sad`, `happy`, `afraid`, …).  Both
     tokens flagged.
  2. **Manner-of-emotion adverbs** — single
     `-ly` adverbs that label emotion outright
     (`angrily`, `sadly`, `nervously`, `wearily`, …).
  3. **Cognition verbs** — single verbs that tell
     the reader the character's interior directly
     (`realised`, `understood`, `knew`, `wondered`,
     `decided`, …).

Hits emit `StyleWarningKind::ShowDontTell` with a
new theme colour `style_warning_show_dont_tell_fg`
(default soft teal `#94e2d5`) — distinct from
filter-word amber + repeated-phrase magenta so the
three overlays stay visually separate when
adjacent.

Patterns are stemmed via the project's Snowball
algorithm so inflections collapse: `seemed`/`seems`/
`seeming` all match the same `seem` linking-verb
stem.  Per-language scaffold ships with English
defaults populated; Russian / French / German /
Spanish lists start empty so the user can fill them
in for the corpus they write in (emotion vocabulary
varies enough per genre and language register that
shipping a default for, say, Russian prose would
mislead more than help).

False-positive control: `was running`, `was reading`,
`looks at the door` — none flagged, because the
2-gram requires both halves to match.  `He was
angry` flags both `was` and `angry`; `He was angry
about something` flags only `was angry`.

**Layer 2: AI-driven scan** (`Ctrl+B Shift+T`)

Sends the open paragraph to the configured LLM
with a system prompt asking for:

  1. Every telling phrase, quoted.
  2. The emotion / state being told.
  3. One concrete show-rewrite per instance.

Response streams into the AI pane.  Reuses the
existing `Inference` plumbing — same patterns as
F12 critique + grammar check.  The prompt name is
`show-dont-tell`; users can override via the
project's prompts book or the global prompts.hjson
the same way they override critique prompts.

Why both layers?  The regex catches the obvious
2-grams (`was angry`, `realised`) instantly while
the writer is still typing.  The AI scan catches
subtler instances — declarative statements,
adverb-modified verbs the regex misses, narration
that explains rather than dramatises — and proposes
specific rewrites.  Together they cover the cheap
loop and the deep loop.

Config (`inkhaven.hjson`):

```hjson
editor: {
  style_warnings: {
    enabled: true
    show_dont_tell: {
      enabled: true
      use_stemming: true
      // English defaults populated automatically;
      // override per language by filling in the
      // per-language lists.
      english_emotion_adjectives: []
      english_cognition_verbs: []
      // … etc.
    }
  }
}
```

Mnemonic: `Shift+T` for *tell* — the AI chord.

### Sentence-rhythm gauge (`Ctrl+B Shift+H`)

A modal that quantifies the rhythm of the open
paragraph and surfaces the felt monotony / variety of
the prose.  Inspired by Gary Provost's well-known
parable:

> "This sentence has five words.  Here are five more
> words.  Five-word sentences are fine.  But several
> together become monotonous.  Listen to what is
> happening.  The writing is getting boring."

What the gauge measures:

  * Splits the paragraph into sentences with a hand-
    rolled walker.  Recognised terminators: `.`,
    `!`, `?` followed by whitespace.  Suppresses
    splits inside common abbreviations (Mr., Mrs.,
    Dr., Sr., Jr., Prof., e.g., i.e., Ph.D., M.D.,
    etc., Inc., Ltd., …).  Treats `...` / `....` as
    a mid-sentence pause, not a terminator.
    Consumes trailing closing quotes + parens so
    `said "Hello."` is one sentence, not two.
  * Tokenises each sentence with the same UAX-#29
    word segmenter that powers the concordance, so
    word counts are consistent across features.
  * Reports per-sentence word count, plus the
    aggregate: mean, population stdev, coefficient
    of variation (CV = stdev / mean).
  * Maps CV to a discrete verdict (with text note
    so the writer doesn't have to remember the
    threshold):

      * CV < 0.25 → **MONOTONE** ("drones · break it
        with a short one")
      * 0.25-0.45 → **STEADY** ("workable but can
        sing louder")
      * 0.45-0.80 → **VARIED** ("strong prose rhythm")
      * ≥ 0.80   → **CHOPPY** ("fragments + long
        sentences mixed")

Why CV, not raw stdev?  CV normalises stdev against
the mean, so a 10-word-mean passage with stdev 5 is
correctly rated identically to a 20-word-mean
passage with stdev 10.  Raw stdev would falsely
favour the longer-sentence text.

Verdict-coloured bar chart (one row per sentence) +
three-shortest + three-longest outlier callouts so
the writer sees *which* sentence is at the extreme,
not just that one exists.  Scroll the bar list with
`↑↓` / `PgUp` / `PgDn` / `Home` / `End`; any other
key closes.

Mnemonic: `Shift+H` for *heartbeat* — the felt
rhythm of the prose.

### POV / character chip on the status bar (`Ctrl+B Shift+P`)

The status bar gains a small chip showing the
characters present in the currently-open paragraph,
with the most-mentioned name surfaced as the
heuristic POV character.

Example status line during a third-person limited
chapter from Anna's POV:

```
[Editor]  POV: Anna  +Bob, Carol   • saved 2s ago …
```

Heuristic:

  * The character mentioned most often in the open
    paragraph wins the POV slot.  Rationale: in
    third-person limited prose, the narrator's gaze
    centres on the POV character, so they're
    inherently the most-named entity in the scene.
  * Ties broken by first-mention order.  When two
    characters appear equally often, the one named
    first is usually the scene anchor.
  * Up to three additional named characters trail
    behind as the supporting cast (DIM-styled `+Bob,
    Carol, Dave`) — capped so the chip stays compact
    on narrow terminals.

Wiring: the chip is driven by the project's existing
`characters` lexicon — the paragraphs nested under
the system-tagged Characters book.  No separate
tagging mechanism, no per-paragraph frontmatter, no
POV annotations to maintain.  Add a character to the
project the same way you always did and the chip
starts surfacing them.

Multilingual: the lexicon already runs every name
through every configured Snowball stemmer (English,
Russian, French, German, Spanish), so `Анна` matches
`Анной` / `Анне` / `Анну` and the count is accurate
across the case system.

Config:

```hjson
editor: {
  pov_chip_enabled: true
}
```

Runtime toggle: `Ctrl+B Shift+P` flips the chip on
/ off without rewriting HJSON.  The toggle is
session-local on top of the persisted setting.

First-person POV is a degenerate case (the narrator
is `I`, who isn't in the character lexicon) — for
those scenes the chip correctly surfaces the *other*
prominent character, which is still useful context
("this scene with Bob from Anna's first-person POV").

### Concordance view (`Ctrl+B Shift+L`)

Project-wide concordance: every distinct lexical stem
in the manuscript, ranked by occurrence count, with up
to three KWIC samples per row so the reader can see
*where* a word actually lives in the prose.  Useful for
spotting overworked vocabulary, locating where a
character or place is mentioned, and noticing the
rhythm of repeated lexical motifs.

Pipeline (one-shot at modal open — cached for the
session of the modal):

  1. Walk every `Paragraph` node in the project's
     hierarchy.
  2. Strip the leading `=` heading line so the title
     doesn't inflate body counts.
  3. Tokenise with `unicode-segmentation`'s UAX-#29
     word segmenter — handles Cyrillic, Latin, Greek,
     and Devanagari word boundaries identically.
  4. Drop single-character tokens, pure-digit runs,
     and configured stop-words.  (Stop-words reuse the
     `editor.style_warnings.repeated_phrases.<lang>_
     stop_words` list — same multilingual setup as the
     repeated-phrase detector, no second list to tune.)
  5. Stem each surviving token via the language's
     Snowball algorithm.  `walk`/`walked`/`walking` all
     key on `walk`; Russian `сказал`/`сказала`/
     `сказали` all key on the same stem.  Disable via
     `editor.style_warnings.repeated_phrases.use_stemming
     = false`.
  6. Group by stem-key, count, and capture the first
     three encountered occurrences as KWIC samples
     (32-char half-width, `«match»` wrapping for
     visual contrast in monospace output).
  7. Sort by count descending, ties broken by headword
     ascending.

Modal interactions:

  * Type to filter — case-insensitive substring match
    against the headword *or* any kept variant.  So
    typing `walk` surfaces an entry whose headword is
    `walked` but whose variants include `walking`.
  * `↑` / `↓` / `PgUp` / `PgDn` / `Home` / `End`
    navigate the list.
  * `Ctrl+S` toggles sort mode (count ↔ alphabetical).
    Plain `s` falls through to the filter input so
    headwords beginning with `s` stay typeable.
  * `Esc` closes.

Performance: well under a second on a 100k-word
manuscript; the work runs synchronously on the UI
thread.  Memory cap: max 3 samples + 5 variants per
stem, so even a Tolstoy-scale corpus stays bounded.

Why the same stop-list as `repeated_phrases`?  Both
features want the same answer to "is this word
lexically meaningful?" — and the user already tunes
that list once for the repeated-phrase detector.
Forking a parallel `concordance.stop_words` table
would force authors to keep two lists in sync.

### Scene-break navigation (`Ctrl+B <` / `Ctrl+B >`)

Jump the editor cursor between scene-break lines in
the open paragraph.  Recognised forms are the
typographic dividers writers actually use:

  `* * *`   `***`   `---`   `___`   `###`   `~~~`   `§`

Any line consisting only of 3+ copies of `*`/`-`/`_`/
`~`/`#` (optionally space-separated) counts.  A lone
`§` (section sign) also counts.  Typst headings
(`= Foo`, `== Foo`) deliberately do NOT count — those
are structural section markers, not scene breaks.

Chord rationale: originally requested as
`Ctrl+B Shift+{` / `Ctrl+B Shift+}`.  The `}` half
collides with 1.2.5's `Ctrl+B }` = TagSearch, so the
binding landed on the vim-style `<` / `>` pair —
both free, same ergonomics.  Existing chord muscle
memory preserved.

### Writing-streak heatmap (`Ctrl+B Shift+G`)

GitHub-style 13×7 grid showing the last 91 days of
project-wide word deltas.  Each cell colored by daily
word count bucket — `·` dim gray for 0, `░` faint
green for 1-249, `▒` medium for 250-499, `▓` bright
for 500-999, `█` solid for 1000+.  Today's cell gets
a dark background highlight.

Footer panel reports:
  * current consecutive-writing-days streak
  * longest streak in the 91-day window
  * total words in the window
  * active days × average words/active day
  * legend

Data source is the existing progress store — the same
daily baselines feeding the startup pulse splash and
`Ctrl+V G` modal.  Captured at open time so the
modal is snappy regardless of paragraph count.  Any
key closes.

Streak computation: count consecutive trailing days
with > 0 words.  Today's 0 is a grace (the user might
still be about to write); a 0 yesterday or earlier
breaks the chain.  Longest = maximum run of > 0 days
within the 91-day window.

### TTS save-as-audio (`Ctrl+B Shift+R`)

Companion to the read-aloud chord.  Writes the open
paragraph to an audio file on disk instead of speaking
through the speakers — useful for building draft
"audiobook" previews, sending a chapter to a beta
reader who prefers listening, or just keeping the
prose available for car listening.

The chord opens a path picker pre-filled with
`<project>/audio/<paragraph-slug>.aiff` (slug derived
from the paragraph title; ASCII-only, hyphenated,
falls back to `paragraph` when there's nothing to
slug).  Enter writes, Esc cancels.  Parent directory
auto-creates if missing.

Uses the same voice + speed as `Ctrl+B S` — pulls
them from `editor.tts.voice` / `editor.tts.speed`.
Output format follows the file extension (`.aiff`
default; `.wav` / `.m4a` also work on macOS 13+).
Status bar reports the written file's size + path.

Internally a thin wrapper over `say -o <path>` — the
same subprocess approach that fixed the TTS reuse
bug.  30-second hard cap on the wait so a stuck
subprocess doesn't hang the TUI.

macOS-only in 1.2.9 (`say -o` is macOS-specific).
Non-macOS hosts see the same "TTS is macOS-only"
modal as `Ctrl+B S`.

### Repeated-phrase detector

Second detector under `tui::style_warnings`.  Slides
an `n`-word window across the open paragraph, stems
each window with the project's Snowball algorithm,
and flags every occurrence of any n-gram that repeats
`threshold` or more times.  Hits drawn in soft magenta
+ underlined so they're visually distinct from
filter-word amber.

Catches writer-crutch gestures: `lifted her shoulders`
recurring 3 times in a chapter; `she felt that he`
echoing across paragraphs; `и в этот момент`
appearing in every other scene.

Multilingual via the existing Snowball setup — no new
per-language tuning needed.  `lifted her shoulders`
matches `lifting her shoulders`; `поднял плечи`
matches `подняла плечи` / `подняли плечи`.

Per-language **stop-word lists** filter closed-class
words (`the`, `и`, `der`, …) before n-gram comparison
so common connectives don't inflate counts.  Built-in
defaults ship for each of the five supported
languages; users extend via
`repeated_phrases.<lang>_stop_words`.

Configuration:

```hjson
editor: {
  style_warnings: {
    enabled: true
    repeated_phrases: {
      enabled: true
      n: 4              // window size; 4 is the
                        // editing-craft sweet spot
      threshold: 3      // 3 occurrences = flag
      use_stemming: true
    }
  }
}
```

Toggles with `Ctrl+B Shift+F` alongside the filter-
word overlay.

### Filter-word style overlay

New inline editor overlay flags **filter words** —
the intensifier crutches and hedges that bloat
sentences without adding meaning.  Toggle in-session
via `Ctrl+B Shift+F` (or set
`editor.style_warnings.enabled: true` in HJSON for
default-on).

Multilingual from day one.  Built-in word lists ship
for **English**, **Russian**, **French**, **German**,
**Spanish**.  The active list is keyed by the
project's top-level `language` field.  Russian writers
get a curated list of `очень`, `просто`, `именно`,
`довольно`, `казалось`, `выглядел`, `вдруг`,
`возможно`, and ~25 more — sized to be useful without
being noise.

Flagged spans render in amber + underlined.  Compose
cleanly with existing overlays (selection, search
match, diff-since-save) — the underline persists; the
other styles stack on top.

User extras layer on top of the language default:

```hjson
editor: {
  style_warnings: {
    enabled: true
    filter_words: {
      enabled: true
      extra_words: ["lifted", "shifted", "blinked"]
    }
  }
}
```

Word matching is Unicode-Standard-Annex-#29 compliant
(via `unicode-segmentation`), so Cyrillic / Latin /
Greek / Devanagari word boundaries all work
correctly — no partial-match false positives
(`justice` won't flag because of `just`).  Columns are
char-indexed so multi-byte text doesn't shift the
underline.

This is the first detector under a new
`tui::style_warnings` module — the same infrastructure
will host repeated-phrase / show-don't-tell /
sentence-rhythm detectors as they land.  Each one
slots in as another `StyleWarningKind` variant and
gets its own theme colour.

### Bund word `ink.tts.speak`

Companion to the chord — `( text -- ) ink.tts.speak`
reads the string at the top of the stack aloud through
the same TTS pipeline `Ctrl+B S` uses.  Non-blocking;
subsequent calls interrupt the prior speech.

  bund> "Hello, world" ink.tts.speak
  bund> "Доброе утро" ink.tts.speak

Useful from hooks (`hook.on_save { drop "saved" ink.tts.speak } register`),
from the Ctrl+B E eval modal, or from user scripts in
the Scripts book.  Lives under the new `audio` policy
category — default-allowed.  Bypass-safe: the feature
is still gated by `editor.tts.enabled` in HJSON, so a
script can't produce audio unless the user opted in.

### Paragraph TTS — `Ctrl+B S` reads aloud

`Ctrl+B S` in the editor pane speaks the open
paragraph through the host OS's TTS engine.  Reading
prose aloud catches awkward phrasing that the eye
glosses over; this puts the muscle-memory chord one
keystroke away.

Cross-platform via the **`tts`** crate
(github.com/ndarilek/tts-rs, 20k downloads/90d):

- **macOS** — AVFoundation (the same engine `say`
  wraps).  Russian voices Milena / Yuri / Katya ship
  free with the OS after a one-time language download.
- **Windows** — SAPI / WinRT.  Russian voices Pavel +
  Irina pre-install on Windows 10+.
- **Linux** — Speech Dispatcher.  Requires
  `apt install speech-dispatcher` system-wide; default
  backend (`espeak-ng`) is robotic — config note in
  CONFIGURATION.md points users to RHVoice / piper for
  natural Russian.

Spirit preservation: no new daemons bundled, no paid
service integration, no model downloads inside the
binary.  macOS + Windows are plug-and-play via OS-
shipped voices; Linux needs one `apt install` and
optional voice upgrade.

HJSON config (default off):

```hjson
editor: {
  tts: {
    enabled: false       // master switch
    voice:   "Milena"    // case-insensitive substring match;
                         // picks "(Enhanced)" / "(Premium)"
                         // variant when installed
    speed:   1.0         // multiplier over engine's normal rate
    greeting: ""         // spoken at startup; empty skips
    goodbye:  ""         // spoken at shutdown; empty skips
  }
}
```

**Greeting + goodbye** (1.2.9, added after initial
TTS landing): when set, inkhaven speaks the greeting
at startup (non-blocking, plays in parallel with the
editor coming up after the daily-progress splash) and
the goodbye at shutdown (blocking up to 5 seconds so
the shell doesn't truncate the audio).  Empty
defaults skip both.  Useful for personal welcomes /
sign-offs — `"Welcome back"`, `"Доброе утро,
Владимир"`, etc.

Chord rebind: editor-scope `Ctrl+B S` was previously a
redundant duplicate of plain `Ctrl+S` (Save).
Reclaimed for `editor.tts_read_paragraph`.  Tree-scope
`Ctrl+B S = AddSubchapter` unchanged.

Modal flow:

- **Disabled in HJSON** → friendly "TTS disabled" modal
  with the exact HJSON snippet to enable.
- **Engine init fails** (Linux without speechd, etc.)
  → "TTS unavailable" modal with the engine error +
  per-platform notes.  Failure is cached so we don't
  pay engine-init cost on every Ctrl+B S.
- **Playback in progress** → floating "Read aloud"
  modal with a spinner, elapsed time, chosen voice,
  and the first 80 chars of the paragraph.  Any key
  (Esc / Space / etc.) stops; the modal auto-closes
  when speech ends naturally.

Internals:

- `App.tts_engine: Option<Result<tts::Tts, String>>` —
  lazy-init, cached on both success and failure.
- Main loop polls `tts.is_speaking()` each frame; the
  TtsPlayback modal closes when the engine reports
  idle.  Cheap call (single FFI boolean) so no need to
  gate it behind a tick.
- Leading typst heading line (`= Title`) stripped from
  the spoken text — it's structural, not prose.
- Voice picker preference: substring-match installed
  voices, then pick the candidate that ALSO contains
  "Enhanced" or "Premium" so `voice: "Milena"`
  auto-upgrades to the premium variant when present.
- Rate: `engine.normal_rate() * cfg.speed` clamped to
  the engine's `[min_rate, max_rate]`.  Per-engine
  normalisation so the user thinks in "1.2x normal"
  rather than wpm numbers that vary per platform.

312 tests pass — the new crate is well-tested upstream
and our integration is a single `Tts::default()` call
plus straightforward speak / stop.

### Ctrl+B W rebranded as "focus mode"

Re-research from the brainstorm caught a smaller issue
than the scratchpad's original framing — `Ctrl+B W` IS
already distraction-free mode (hides Tree / AI / Search /
AI-prompt, forces editor focus), but the codename
"typewriter mode" buries this fact from new users, and
the chord was **completely undocumented** in
KEYBINDING.md.

Changes:

  - `Action::ToggleTypewriter` short label
    "typewriter" → "focus mode".
  - Long help re-written to call this
    "distraction-free / focus mode" explicitly, with a
    one-line note that the HJSON serde key
    (`global.toggle_typewriter`) stays for backward
    compatibility.
  - Status-bar strings ("typewriter mode · Ctrl+B W to
    exit" / "typewriter mode off") rewritten to use
    "focus mode".
  - New row in `KEYBINDING.md` table — previously missing
    entirely.
  - Tutorial 03 (the editor) gets a "Focus mode
    (distraction-free)" section between "The visual
    overlays" and "Find and replace (regex)".

Not changed (intentional):

  - `Action::ToggleTypewriter` enum variant name —
    public via `keys.bindings`; renaming would break
    user HJSON overrides.
  - Serde key `global.toggle_typewriter` — same reason.
  - Internal field `App.typewriter_mode` + the
    `toggle_typewriter_mode()` method — cosmetic; the
    legacy name doesn't surface to users anywhere
    after this commit.

### "Wiki-links" terminology pass

Renamed across active docs + source code.  The chord
set (`Ctrl+V A` / `I` / `L` / `K`), the `linked_paragraphs`
metadata field, and the file `Documentation/Tutorials/19-wiki-links.md`
are all unchanged (the last one keeps its filename so
external bookmarks don't break — its H1 title is the
new "Paragraph links and backlinks" with a disambiguation
note up top).

Scope intentionally limited:

  - Active documentation: KEYBINDING.md, CONFIGURATION.md,
    KEYS_REASSIGNMENT.md, Documentation/README.md,
    Tutorials/README.md, Tutorials 19 / 26 / 32 / 33 / 37,
    top-level README.md.
  - Source code: src/store/{mod,node}.rs, src/story_view.rs,
    src/tui/{app,keybind,state}.rs.

NOT touched (deliberate):

  - `Book/` — the in-project content book stays intact per
    project policy.
  - `Documentation/RELEASE_NOTES/1.2.4.md` through
    `1.2.7.md` — historical release notes are an
    immutable record of what shipped under what name at
    that time.
  - `Documentation/RELEASE_NOTES/README.md` — release
    index references historical titles by their
    contemporary names.

## Still on the table

(see "Planned themes" above)

## Smaller changes

Currently empty. Will fill in as work lands.

## Migration

No breaking changes planned. 1.2.9 is intended as a
polish cycle on top of 1.2.8; existing projects upgrade
transparently.

## Statistics

To be filled in at release.

## What's next

1.3 — full-screen split-view ergonomics and the F4
split-edit workflow.

— inkhaven 1.2.9, in development.
