# Inkhaven 1.2.14 — plot intelligence + inline comments + quick wins

1.2.13 was the **Language book** release — a
first-class invented-language workbench with
five-chapter sub-books (Meta / Dictionary /
Grammar / Phonology / Sample texts), HJSON-
fronted entries, lexicon overlay + status chip
in the manuscript editor, AI translation flow
with `Ctrl+B Q` / `Ctrl+B Shift+Q`, full CLI
surface (init, add-word, --import CSV with --new
/ --force, remove-word, list, doctor with --json,
export), and two tutorials (49 + 50) with a
worked Tira sample CSV.

1.2.14 picks up two new themes plus a sweep of
small quick-wins, in three groups:

* **Theme A — Plot / narrative intelligence.**
  New `Threads` system book, surfaced via
  `Ctrl+V Shift+H` (picker + 5-thread swim-lane
  weave view), `Ctrl+V Shift+A` (AI thread audit),
  `Ctrl+V Shift+D` (deterministic thread doctor),
  and the `inkhaven thread` CLI (`add` / `list` /
  `doctor` / `export --format json|csv|markdown`).
  Threads tie waypoints to manuscript paragraphs
  so dropped payoffs and dormant arcs surface
  before the reader sees them.

* **Theme C — Inline comments.**  Sidecar JSON
  storage so `<paragraph>.comments.json` travels
  with the prose in git and diffs cleanly.
  `Ctrl+V c` adds a span-anchored comment (char-
  offset, not byte, for UTF-8 safety); the editor
  renders the span with `theme.comment_span_modifier`
  and surfaces the comment text + author + age in
  the footer when the cursor is inside.  `Ctrl+V
  Shift+C` opens the project-wide comments panel
  with breadcrumb / author / age / snippet columns
  plus a `(N/M in ¶)` dense indicator; panel chords
  include `Enter` open + jump cursor, `r` resolve,
  `R` toggle resolved-filter, `d` delete, `/`
  filter, `a` AI digest (categorised across
  STRUCTURAL / PROSE / FACTUAL / QUESTION).  Full
  CLI surface: `inkhaven comments list / resolve /
  reopen / delete / export`.

* **Quick wins.**  Project templates (`inkhaven
  init <path> --template <name>` + `inkhaven
  template list`) shipping 14 scaffolds, including
  5 Russian (novel, повесть, научная фантастика,
  лор, утопия) + 3 international (epic-fantasy,
  mystery, french-novel).  HJSON-driven snippet
  expansion in the editor with `{date}`, `{time}`,
  `{slug}`, `{book}`, `{chapter}`, `{author}`,
  `{cursor}` placeholders triggered on Space.  AI
  continuation drafting (`Ctrl+V d` — voice-anchor
  envelope with `[[CURSOR_HERE]]` marker, response
  wrapped in `<<<DRAFT>>>`).  Inline footnote
  insertion (`Ctrl+V f`) — Typst (default) or
  markdown markup.  Project word-count goal modal
  (`Ctrl+V Shift+G`) with 30-day projection +
  ahead/on-track/behind/complete verdict.  Style-
  transfer rewrite (`Ctrl+V y`) from a recent-
  paragraph picker.

637 tests pass (was 565 in 1.2.13 — +72).  Zero
new dependencies.

See `Documentation/PROPOSALS/1.2.14_PLAN.md` for
the original proposal that drove this cycle.

## Planned themes

### Carryovers from 1.2.13

#### Language book — Phase D.2 (deferred from 1.2.13)

  * **`--format grammar` export** — printable
    grammar reference (TOC + chapter per rule
    category + examples tables + cross-refs).
    The current grammar HJSON template carries
    the right shape; the exporter needs to
    parse + format it.
  * **`--format phrasebook` export** — Sample-
    text chapter rendered as a phrasebook,
    gloss on left, working language on right.
  * **`--format csv` export** — round-trip-
    compatible CSV that the `--import` path
    can re-ingest.  Closes the import/export
    loop.
  * **`inkhaven language test <name>`** —
    headless roundtrip translation drift CLI.
    Walks a corpus of sample texts through
    `Ctrl+B Q` → `Ctrl+B Shift+Q` and reports
    drift.  Catches grammar-rule inconsistencies
    before they bite in the manuscript.  The
    in-TUI version of this workflow already
    works; this CLI version is for CI gates.
  * **`inkhaven language translate <language>
    <text>`** — headless flavour of the in-TUI
    `Ctrl+B Q` chord.  Useful for shell-pipe-
    driven authoring.
  * **`inkhaven language define-rule <language>
    <rule_id> --category <cat>`** — open the
    rule template in `$EDITOR` for hand-editing
    the HJSON.  Pairs with the deferred
    `--format grammar` exporter.

#### Language book — UX polish

  * **`Ctrl+B Shift+R` reverse-lookup picker
    in Language book** — find dictionary entry
    by translation.  The `LanguageEntryIndex`
    from 1.2.13 already keys by surface form;
    just needs an inverse `translation → entry`
    index + a picker modal mirroring `LlmPicker`.
  * **`Ctrl+B Shift+W` word-of-the-day card**
    in the manuscript editor — random Dictionary
    entry as a floating card.  Pairs with the
    `Ctrl+B Shift+W` phonotactic generator in
    the Language book (generate a candidate
    word conforming to the language's
    phonotactics).
  * **Card renderers** for Dictionary / Grammar
    / Phonology paragraphs viewed inside the
    Language book — the §7 / §10 visualisations
    from the proposal.  Significant render work.
  * **Inline gloss-table renderer** — the AI
    pane currently renders the LLM's markdown
    table as-is.  A dedicated renderer that
    aligns the `source · gloss · target`
    columns with column-typed colours would
    make the gloss easier to scan during
    proofreading.
  * **Confidence-flag chip** — when the LLM
    returns "dictionary missing for X", a
    one-key chord to jump straight into
    `inkhaven language add-word`'s seed prompt
    would close the diagnose → fix loop.
  * **Inflection-aware status-bar chip** —
    when the cursor lands on an inflected form
    (not the lemma), surface the paradigm name
    alongside the lemma + translation:
    `[atatal (plural of atal) · noun · river]`.

### Carryovers from 1.2.12

  * **Mouse-driven scene-break selection** —
    deferred from 1.2.11 → 1.2.12 → 1.2.13;
    still on the list.  Drag from one
    scene-break line to the next selects the
    scene.  Pairs with the existing
    `Ctrl+B <` / `Ctrl+B >` scene-break
    navigation.

  * **Inline streaming-response pane inside
    the `Ctrl+B 0` HJSON editor.**  The 1.2.12
    LLM review (`Ctrl+R`) routes the response
    into `App.inference` — visible in the AI
    pane only after closing the modal.  Carving
    the bottom 1/3 of the HJSON-editor modal
    for an inline response pane would let the
    user critique + fix without context-
    switching.

  * **Full per-overlay parity for the split-
    view secondary editor.**  Search highlights,
    style-warning detectors, lexicon highlights,
    grammar overlays, tag overlays all run on
    the primary's `draw_editor` but not on
    `draw_secondary_editor`.  In practice OK
    for translation / reference workflows but a
    future commit could thread the overlay
    passes through.

### Other candidates

  * **Sixth language for the supported set** —
    Italian / Portuguese / Japanese.
    Substantial content lift: stop-words +
    Snowball stemmer plumbing (where available)
    + SDT vocabulary lists + embedded prompt
    set.  Bootstrap CLI already exists.

  * **More "diagnose → AI fix" pairs.**  The
    1.2.11 rhythm-rewrite chord
    (`Ctrl+B Shift+M`) pairs with the rhythm
    gauge (`Ctrl+B Shift+H`); same shape could
    apply to repeated-phrase detector → AI
    rewrite, weak-verb detector → AI replace,
    etc.

  * **Live theme preview in config TUI.**  The
    HSL slider shipped in 1.2.11 but its
    preview is widget-local.  Applying the
    candidate colour to the live editor pane
    would catch contrast issues without saving
    + restarting.

  * **GitHub Release object follow-up.**  1.2.11,
    1.2.12, and 1.2.13 tags pushed but the
    Release UI object wasn't created (gh CLI
    auth lacked `workflow` scope at release
    time).  Backfill all three, then either
    keep refreshing the scope before each
    release or automate via
    `.github/workflows/release.yml`.

## Implementation log

### Threads system book — Phase A.1 (foundation)

Lays the groundwork for the plot-intelligence
tooling described in
`Documentation/PROPOSALS/1.2.14_PLAN.md`.  Phase
A.1 is the storage + scaffold layer only — no
weave view (A.2), no AI thread audit (A.3).

* **`SYSTEM_TAG_THREADS = "threads"`** added to
  `src/store/mod.rs`.  Top-level `Threads` system
  book auto-creates on next project open (or
  upgrade from a pre-1.2.14 project) alongside the
  existing system books.  Lands at canonical order
  index 6 — after Artefacts, before Language — so
  the worldbuilding cluster (Places / Characters /
  Artefacts / Threads / Language) groups together
  in the tree.  Existing user books shift up by 1
  if they collided with the slot; the heal-pass
  at the end of `ensure_system_books` mops up.

* **`inkhaven thread add <name>`** CLI subcommand.
  Creates a Paragraph under the Threads system
  book, seeds the body with the full commented
  HJSON template per proposal §3.1 (identity:
  `title` / `status` / `weight` pre-filled from
  the CLI flags; arc shape: `opening` /
  `midpoint` / `payoff`; connections:
  `characters` / `places` / `artefacts` /
  `related_threads`; metadata: `tension` /
  `register` / `notes`).  Sets `content_type =
  "hjson"` so the editor renders with syntax
  highlighting + the paragraph status bar shows
  `[hjson]`.  Disk-write + bdslib-write follow
  the canonical two-step pattern from Language
  entries.  Rejects duplicates by case-insensitive
  title.

  ```
  $ inkhaven thread add "inheritance subplot" \
      --status develop --weight subplot
  added thread `inheritance subplot` to Threads (develop · subplot)
    open Threads/inheritance subplot in the editor to fill opening / midpoint / payoff
  ```

* **`inkhaven thread list`** CLI subcommand.
  Walks the Threads subtree, parses each entry's
  HJSON summary fields, prints a column-aligned
  table:

  ```
  $ inkhaven thread list --status develop
    name                  status    weight  tension   ch  pl  art
    ----------------------------------------------------------
    inheritance subplot   develop  subplot        7    2   1    0
    redemption arc        develop    major        5    1   0    1
  ```

  `--status` / `--weight` flags filter case-
  insensitively.

* **Tree-pane `+` chord under Threads** —
  `commit_add` now detects when the parent's
  ancestor chain includes the Threads system book
  and seeds the body with the thread HJSON
  template (`title` pre-filled from the typed
  name; `status: "setup"`, `weight: "major"`).
  `content_type` flips to `"hjson"`.  Same
  template-seed pattern Language Grammar/Phonology
  paragraphs use, just with a different schema.
  No bucket-routing needed — threads are flat
  Paragraphs under the system book (or chapters
  the author adds for grouping).

* **Phase A.1 tests** — 7 new:
  `seed_thread_body_includes_core_fields`,
  `seed_thread_body_uses_name_when_title_omitted`,
  `seed_thread_body_prefers_explicit_title`,
  `seed_thread_body_parses_as_valid_hjson`,
  `parse_thread_summary_extracts_filter_fields`,
  `parse_thread_summary_returns_none_on_empty`,
  `escape_hjson_handles_quotes_and_backslashes`.

572 tests pass (was 565 — +7).  Zero new
dependencies.  Phase A.2 (thread weave view +
`Ctrl+V Shift+H` picker) and Phase A.3 (AI thread
audit + `Ctrl+V Shift+A`) are queued.

### Threads — Phase A.2 (picker + weave view)

The first user-facing payoff of the Threads
system book.  Project-wide picker over every
plot-thread paragraph, with a swim-lane weave
sub-view that surfaces which chapters advance
which threads.

* **`Ctrl+V Shift+H`** opens the **Threads
  picker** (action: `view.threads_picker`).
  Centred modal listing every thread with
  columns: name (title field from HJSON;
  falls back to slug-derived paragraph title)
  · status · weight · tension · character
  link count · place link count ·
  reverse-link count (how many manuscript
  paragraphs link TO this thread via the
  existing `Ctrl+V A` outgoing-link
  mechanism).

  Picker chords:
  * `↑ ↓ Home End` — navigate the visible
    rows.
  * `Enter` — open the thread entry in the
    editor.
  * `Shift+Enter` — pin to the split-view
    secondary slot (mirrors the existing
    universal Shift+Enter pin modifier from
    1.2.12).
  * `/` — enter filter mode; typed substring
    matches against name + title + status +
    weight (case-insensitive).  `Enter` /
    `Esc` exit filter mode without closing the
    picker.
  * `w` — open the weave-view sub-modal.
  * `Esc` — close picker.

  Reverse-link counts are tallied in a single
  pass over the hierarchy at picker-open time
  so we don't pay `O(threads × paragraphs)`
  per render.

* **`w` from picker** opens the **swim-lane
  weave view** — a `Modal::ThreadWeaveView`
  pushed with the picker stored in
  `return_to` so `Esc` pops back.  Layout:

  ```
   ┌─ Thread weave · 4 threads × 12 chapters ──────┐
   │                man         man-ru             │
   │                 1  2  3  4  5  6  7  8  9 ...  │
   │ inheritance   · ●  ●  ·  ·  ●  ·  ·  ·  ●  ·   │
   │ secret-cult   ·  ·  ●  2  ·  3  ●  ·  ●  ·   │
   │ redemption    ●  ·  ·  ·  ●  ·  ●  ·  ·  ●   │
   │ comic-relief  ·  ·  ·  ·  ·  ·  ·  ●  ·  ·   │
   │                                                │
   │ inheritance subplot · manuscript-en/05-rain · 1 │
   │ ↑↓ thread · ←→ chapter · Enter jump · Esc back │
   └────────────────────────────────────────────────┘
  ```

  Cell content: `·` for zero links, `●` for
  one, the count for two or more.  Cursor
  cell is reversed; non-empty cells colour
  with `theme.places_fg` to match the
  manuscript overlay convention.  Book
  grouping shown in a header row above the
  chapter index numbers (3-letter book
  abbreviation, repeated only on book
  boundaries to avoid visual noise).

  Weave-view chords:
  * `↑ ↓` — move between threads.
  * `← →` — move between chapters.
  * `Home End` — jump to first / last
    chapter.
  * `Enter` — open the first linking
    paragraph in the cursor cell.  Status
    message when the cell is empty.
  * `Esc` — return to the picker (modal stack
    via `return_to: Box<Modal>`).

  Footer line tracks the cursor coordinate:
  `<thread title> · <book>/<chapter> · N
  linking paragraphs`.

  Grid pre-computed at modal-open time so
  navigation is pure cursor math; no per-
  keystroke hierarchy walks.

* **Surfaces:**
  * `Action::ViewThreadsPicker` — rebindable
    via the standard `keys.bindings` HJSON
    chord-customisation channel.
  * `Modal::ThreadsPicker` + `Modal::ThreadWeaveView`
    + `ThreadsPickerEntry` struct in
    `src/tui/modal.rs`.
  * `src/tui/app/threads_impl.rs` — new
    sub-module of `tui::app` carrying the
    entry-collection + reverse-link tally +
    picker handler + weave handler.
  * `draw_threads_picker_modal` +
    `draw_thread_weave_modal` in
    `src/tui/app/render/modals.rs`.

* **Phase A.2 tests** — 4 new pinning the
  new `truncate_to` Unicode-safe helper:
  `keeps_short_strings_intact`,
  `truncates_with_ellipsis`,
  `handles_max_one_or_zero`,
  `unicode_safe_no_byte_split` (Cyrillic 2-byte
  chars).

576 tests pass (was 572 — +4).  Zero new
dependencies.  Phase A.3 (AI thread audit +
`Ctrl+V Shift+A`) queued.

### Threads — Phase A.3 (AI thread audit)

The headline AI feature of the plot-intelligence
theme.  Reads every Thread paragraph's HJSON +
runs a structural blind-spots pre-pass + walks
the cursor's scope, then asks the LLM to score
each scope paragraph against the thread
inventory and call out drift.

* **`Ctrl+V Shift+A`** (action:
  `ai.thread_audit`) — fires the audit from any
  pane.

* **Scope resolution from the F9 AiMode chip:**
  * `AiMode::Book` / `Chapter` / `Subchapter`
    → walk up from the cursor (open paragraph
    when there is one, otherwise the tree-pane
    cursor) until a node of that kind is found.
  * `AiMode::Paragraph` / `Selection` / `None`
    → fall back to the cursor's containing
    Chapter.  Auditing a single paragraph is too
    narrow to be useful; the chapter is the
    smallest meaningful unit.
  * System-tagged books are skipped (auditing
    the Threads book against itself is a
    no-op).

* **Blind-spots pre-pass** — computed BEFORE the
  LLM ever runs so the model has hard evidence
  rather than guessing from prose alone.  Three
  detectors:
  * **Dormant in scope** — thread has links
    elsewhere in the project but zero in the
    cursor scope.  Flags arcs the section is
    neglecting.
  * **Payoff unfired** — thread status is
    `payoff` but no paragraph in the project
    links to it.  Surfaces declared payoffs the
    author hasn't actually written yet.
  * **Zero links** — thread status is past
    `setup` but the project contains zero
    references.  Catches the "I declared this
    thread, but never followed through" gap.

* **Prompt envelope** composer reads:
  * Every Thread paragraph's full HJSON body
    (title / status / weight / arc shape /
    connections / tension / register / notes).
  * The blind-spots list with the kind label
    for each.
  * Every paragraph in the scope (title + body +
    outgoing-link count).
  * A system prompt that asks for four-part
    output:
    1. Per-paragraph thread scoring (advances /
       touches incidentally / should advance
       but doesn't).
    2. Confirm or refute each blind spot.
    3. Flag threads whose declared status looks
       wrong given the manuscript evidence
       (e.g. `setup` with many incidental
       touches; `payoff` without the payoff
       landed).
    4. One-paragraph summary of the scope's
       narrative health.

* **Streaming** — runs through the shared
  `spawn_chat_stream` + `Inference` machinery
  (same plumbing every other AI flow uses).
  Focus jumps to the AI pane; the response
  streams in.  Standard `I` / `T` / `B` apply
  chords work for selectively pulling sections
  of the audit into a Notes paragraph.

* **No new tests** — Phase A.3 is integration
  glue (HJSON parsing covered by Phase A.1;
  hierarchy walking covered by existing tree
  tests; envelope composition has no isolatable
  pure-logic surface).  576 tests still pass.

* **Implementation surfaces:**
  * `Action::AiThreadAudit` — rebindable via
    `keys.bindings`.
  * `App::start_thread_audit` +
    `resolve_thread_audit_scope` +
    `collect_scope_paragraphs` +
    `compute_blind_spots` +
    `collect_thread_bodies_full` +
    `compose_thread_audit_prompt` in
    `src/tui/app/threads_impl.rs`.
  * `ScopedParagraph` + `BlindSpot` /
    `BlindSpotKind` private types in the same
    module.

**Phase A complete.**  Threads system book +
HJSON template + TUI seed + CLI (A.1), picker +
swim-lane weave view (A.2), AI thread audit
(A.3).  The plot-intelligence anchor for 1.2.14
is shipped.

### Comments — Phase C.1 (sidecar storage + Ctrl+V c)

Inline comments anchored to character spans in
manuscript prose.  Span anchoring, sidecar JSON
storage that travels with the .typ in git, a
multi-line editor modal, and a status-bar chip
when the cursor lands in a commented span.

* **`Ctrl+V c`** (action: `view.add_comment`) —
  fires from the editor.  Resolves the anchor
  span:
  * **With selection** — anchor is the
    selection's character range, normalised
    so a reverse selection (end before start)
    still produces a `(start, end)` pair where
    `start < end`.
  * **No selection** — anchor is the word at
    the cursor, walked outward via Unicode
    `is_alphanumeric() || '_' || '\''`
    boundaries.  When the cursor is on a blank
    line or whitespace, the chord errors with
    "place cursor on a word OR select a span".

* **`Modal::CommentEditor`** — multi-line
  `tui-textarea` body with a header line
  showing the anchor span snippet ("on:
  \"…\"") so the author sees what they're
  commenting on.  Chords:
  * `Ctrl+S` — commit.
  * `Ctrl+C` — cancel unconditionally.
  * `Esc` — cancel when the buffer is empty;
    otherwise no-op (prevents accidental loss
    of long-typed comments).
  * Everything else is forwarded to the
    textarea so the author can type / navigate
    normally.

* **Sidecar JSON storage** — each paragraph's
  comments live in a sidecar file alongside
  the `.typ`:

  ```
  books/manuscript/chapter-1/03-rain.typ
  books/manuscript/chapter-1/03-rain.comments.json
  ```

  Schema version 1:

  ```json
  {
    "schema_version": 1,
    "comments": [
      {
        "id": "...",
        "char_start": 412,
        "char_end": 487,
        "author": "vladimir",
        "created_at": "2026-06-01T10:23:00Z",
        "resolved": false,
        "resolved_at": null,
        "text": "…",
        "replies": []
      }
    ]
  }
  ```

  * **Character offsets**, not byte — UTF-8
    boundary edits don't corrupt anchoring.
  * **Stable UUID** per comment so an AI digest
    or beta-reader export survives author
    edits to surrounding prose.
  * **Pretty-printed** JSON for git-friendly
    diffs.
  * **Empty comments list** → sidecar removed
    (no littering the project with empty JSON
    files).
  * **`replies: []`** reserved for Phase C.2
    threading.

* **Author resolution** — `editor.comment_author`
  HJSON field overrides; falls through to
  `$USER` → `$LOGNAME` → `$HOSTNAME` →
  `"anonymous"`.  Set per-project when the
  inferred author is wrong (shared workstation,
  system account) or when multi-author
  attribution matters.

* **Footer chip** — when the cursor lands inside
  a commented span, the editor's one-row
  footer (shared with the goal gauge + the
  Phase B.2 Language chip) shows:

  ```
  [vladimir · 75 chars · 2d ago] Does this sentence land the redemption beat?
  ```

  Comment chip takes priority over both other
  footer chips — explicit reviewer attention
  the author should see first.  Styled with
  `theme.comment_span_modifier` (default
  `underline+italic`).

* **`theme.comment_span_modifier`** — new HJSON
  knob mirroring the existing
  `style_warning_*_modifier` fields.  Accepts
  `+`-combined tokens (`bold`, `dim`, `italic`,
  `underline`, `reversed`, `none`); empty
  string keeps the default.

* **OpenedDoc** carries a `comments:
  CommentsFile` field loaded at paragraph-open
  time + mutated on `Ctrl+V c` commit.
  Secondary slot (split view / similar-mode)
  also loads its own; snapshot slot stays
  empty (read-only by design).

* **Phase C.1 tests** — 14 new in
  `tui::comments::tests`:
  * `sidecar_path_swaps_extension` /
    `_handles_non_typ_extension`
  * `char_offset_to_row_col_basic` /
    `row_col_roundtrip` /
    `char_offset_handles_unicode`
  * `derive_anchor_uses_selection_when_present`
    /
    `derive_anchor_normalises_reverse_selection`
    /
    `derive_anchor_falls_back_to_word_at_cursor`
    / `derive_anchor_returns_none_on_blank`
  * `per_row_hits_single_line_span` /
    `per_row_hits_multiline_span`
  * `comment_at_cursor_finds_containing_comment`
  * `resolve_author_uses_configured_when_set`
  * `comments_file_roundtrip`

590 tests pass (was 576 — +14).  Zero new
dependencies (chrono + uuid + serde_json already
in-tree).

**Phase C.1 follow-up — Phase C.1.1**
(deferred, available helpers already in
`comments.rs`):

  * **Inline span overlay** — paint the
    commented span itself with
    `theme.comment_span_modifier` so the
    author sees which prose has comments
    WITHOUT navigating the cursor onto each
    one.  Threads through `build_row_spans` in
    `tui::highlight` — adds a
    `comment_hits: &[CommentRowHit]` parameter
    + the merge logic.  `per_row_hits()` +
    `RowHit` already live in
    `tui::comments`; just need the wire-up.
  * **Resolved-comment dim rendering** — the
    sidecar already carries `resolved: bool`;
    the chip already labels it `[resolved]`.
    Phase C.1.1 dims the inline span
    rendering for resolved comments instead
    of `comment_span_modifier`.

**Phase C.2 queued** (per proposal §4.4–§4.5):
comments panel `Ctrl+V Shift+C` (list /
filter / resolve / unresolve / AI digest /
navigation), `inkhaven comments …` CLI
(list / add / resolve / delete / export).

### Q.1 — project templates

Six bundled templates accelerate the
"I-have-an-idea" → first-paragraph step.

`inkhaven init <path> --template <name>` runs
the standard scaffold (config + prompts +
system books) THEN walks the named template's
data to create the user manuscript book +
chapters + system-book seed entries.  Errors
during template application surface upward
but don't roll back the standard init —
partial scaffolding is recoverable via
`inkhaven add`; a rolled-back init isn't.

Templates:

* **`empty`** (default) — current behavior, no
  extra scaffolding.
* **`novel`** — three-act manuscript (`Act I —
  Setup` / `Act II — Confrontation` / `Act III
  — Resolution`) + Characters book seeded with
  `protagonist` / `antagonist` / `confidant`
  stubs.  Recommended word-count goal: 80000.
* **`nonfiction`** — manuscript with
  `Introduction` / `Part I` / `Part II` /
  `Conclusion` chapters + Research book
  seeded with a `methodology` paragraph.
  Recommended word-count goal: 60000.
* **`rpg-sourcebook`** — `Sourcebook` book
  with `Setting` / `Rules` / `Adventures` /
  `Appendices` + Places / Artefacts / Threads
  each seeded with one example entry (the
  Threads seed is a valid HJSON paragraph
  ready to fill).  Recommended word-count goal:
  120000.
* **`technical`** — `Documentation` book with
  `Overview` / `Reference` / `Tutorials` /
  `Index` chapters.  No word-count goal
  default (technical docs are bounded by
  topic coverage).
* **`nanowrimo`** — same shape as `novel`
  but the post-init message recommends a
  50000-word goal + `target_date: <next Nov
  30>` + the 1667-words/day pacing math.
  Pairs with the Q.4 project goal modal
  (queued).

* **`inkhaven template list`** — two-column
  table (name + description) with each
  template's recommended use case + word-
  count goal.

* **Surfaces:**
  * `Command::Template(TemplateCommand::List)`
    — rebindable via `keys.bindings` if anyone
    wires a TUI chord; currently shell-only.
  * `src/cli/templates.rs` — `ProjectTemplate`
    / `ManuscriptBook` / `SystemBookSeed`
    structs, six `const` definitions, the
    `apply()` walker that creates the user
    book + chapters + system seeds.
  * `cli::init::run` signature gained a
    `template: &str` parameter; the
    `Command::Init` variant gained
    `template: String` (default `"empty"`).

* **Phase Q.1 tests** — 8 new in
  `cli::templates::tests`:
  * `registry_contains_every_named_template`
  * `empty_template_has_no_scaffolding`
  * `novel_template_has_three_act_structure`
  * `rpg_template_seeds_places_artefacts_threads`
  * `nanowrimo_template_inherits_novel_seeds`
  * `wrap_description_handles_short_strings`
  * `wrap_description_wraps_long_strings`
  * `rpg_thread_seed_parses_as_hjson`

598 tests pass (was 590 — +8).  Zero new
dependencies.

### Comments — Phase C.1.1 (inline span overlay)

Resolves the last C.1 deferred item: comment
spans now visibly highlight in the editor, so
authors see WHICH prose has comments without
moving the cursor onto each one.  Resolved
comments dim instead of underline to keep
historical context visible without active-
noise.

* **Threaded `comment_hits: &[RowHit]`** through
  the highlighter:
  * `build_row_spans` — unwrapped editor.
  * `build_visual_row_spans` — wrapped editor.
  * Both pick up a new `comment_style_at`
    helper analogous to `match_style_at` /
    `lex_style_at` / `style_warning_style_at`.

* **Per-row comment hits** computed once at
  draw time via `comments::per_row_hits()` —
  the helper already existed in `tui::comments`
  but was unused; Phase C.1.1 lights it up.
  Empty fast-path when the open paragraph
  carries no comments (the common case — most
  paragraphs do).

* **Resolved-aware styling** — the existing
  `Comment.resolved` flag now drives the
  overlay:
  * Active comments: `theme.comment_span_modifier`
    (default `underline+italic`).
  * Resolved comments: `Modifier::DIM`.

  The footer chip already labels resolved
  comments `[resolved]` (Phase C.1) — the
  inline overlay now matches.

* **Phase C.1.1 tests** — 1 new:
  `per_row_hits_propagates_resolved_flag`.
  The other C.1 tests (sidecar IO, anchor
  derivation, etc.) already pin the upstream
  data shape; the overlay change is rendering-
  only.

599 tests pass (was 598 — +1).  Zero new
dependencies.

**Phase C complete for 1.2.14 unless C.2
(comments panel + CLI) lands.**  Comment
authoring round-trip — anchor, write,
visualise, surface in the footer chip, see
resolution state inline — works end-to-end.

### Q.2 — snippet expansion (HJSON-driven)

`editor.snippets` HJSON stanza maps trigger
strings to expansion bodies.  When the editor
sees a "non-word" character (whitespace,
sentence-ending punctuation, closing bracket,
newline, tab) typed immediately after a
configured trigger, the trigger is replaced
inline with the resolved expansion; the
triggering non-word char stays so `\dt `
becomes `<date> ` with the space intact.

**HJSON shape:**

```hjson
editor: {
  snippets: {
    enabled: true
    triggers: {
      "\\dt":   "{today}"
      "\\time": "{now}"
      "\\sig":  "— {author}"
      "\\tk":   "[[{paragraph_slug}]]"
      "\\sel":  "<<{selection}>>"
      "\\dts":  "{today:%Y-%m-%dT%H:%M:%S}"
    }
  }
}
```

`enabled` defaults to `true`; an empty
`triggers` map means no expansion fires.

**Placeholders** resolved in expansion bodies:

| Placeholder | Replaced with |
|-|-|
| `{today}` | Today's date `YYYY-MM-DD` |
| `{today:%FMT}` | Today's date in the chrono format string |
| `{now}` | Current time `HH:MM` |
| `{now:%FMT}` | Current time in the chrono format string |
| `{paragraph_title}` | The open paragraph's display title |
| `{paragraph_slug}` | The open paragraph's slug |
| `{selection}` | The active selection text (empty when none) |
| `{author}` | Resolved comment author (mirror of `editor.comment_author`) |

Unknown placeholders pass through verbatim
(`{nonexistent}` stays `{nonexistent}`) so
typos are visible to the author.  Unmatched
braces also pass through.

**Trigger matching:**
* Longest-match wins (so `\dts` beats `\dt`
  when both could fire).
* Case-sensitive substring at the END of the
  text before the firing char.
* Trigger characters typed without a firing
  non-word character don't expand — they
  accumulate until the user commits with a
  space / period / Enter / Tab / etc.

**Wiring:**
* `tui::snippets` module — `ExpansionContext`,
  `find_trigger`, `expand_placeholders`,
  `is_expansion_trigger_char`.
* `App::maybe_expand_snippet` (new method on
  `tui::app::editor_impl`) fires from
  `handle_editor_key_inner` AFTER the textarea
  processes the trigger char.  Short-circuits
  on: snippets disabled, no triggers, no
  paragraph open, read-only paragraph, cursor
  at column 0, no trigger match.
* `read_selection_text` helper extracts the
  selection into the `{selection}` placeholder
  (single-line + multi-line both handled).
* Trigger replacement uses `start_selection`
  + `move_cursor(Back)` × N + `cut` +
  `set_yank_text` + `paste` — the canonical
  "replace" pattern used elsewhere in the
  crate.

**Deferred to Phase Q.2.1:**
* `{cursor}` — re-position cursor after
  expansion (multi-position logic).
* Picker-based placeholders: `{char_lookup}`,
  `{place_lookup}`, `{artefact_lookup}`.
* `bund:` prefix — route the expansion body
  through the embedded Bund VM for
  context-aware expansion.

**Phase Q.2 tests** — 11 new:
* `find_trigger_matches_at_end` /
  `_returns_none_when_no_match` /
  `_prefers_longer_match`
* `expand_static_placeholders` /
  `_today_default_format` /
  `_chrono_format_suffix` /
  `_unknown_placeholder_passes_through` /
  `_unmatched_brace_passes_through` /
  `_literal_braces_in_body`
* `expansion_trigger_char_recognises_common_punctuation`
  / `_skips_word_chars`

610 tests pass (was 599 — +11).  Zero new
dependencies.

### Comments — Phase C.2 (panel + CLI)

Project-wide review surface for inline comments,
in both the TUI and headless paths.

* **`Ctrl+V Shift+C`** (action:
  `view.comments_panel`) opens the project-wide
  comments panel.  Walks every paragraph's
  `.comments.json` sidecar at open time and
  lists every comment with breadcrumb / author /
  age / text-snippet rows.  Sorted by
  paragraph slug-path, then newest comment
  first within each paragraph group.

  Panel chords:
  * `↑↓ Home End` — navigate visible rows.
  * `Enter` — open the source paragraph in the
    editor.  Phase C.2.1 will also position
    the cursor at the comment's `char_start`.
  * `r` — resolve the cursor's comment.
    Writes the sidecar IMMEDIATELY (no batch-
    on-close) so a crash mid-review doesn't
    lose decisions.
  * `u` — un-resolve (reopen) the cursor's
    comment.
  * `R` — toggle the "hide resolved" filter.
    Default is hidden — the panel focuses on
    actionable work.
  * `d` — delete (immediate, no confirm; UUID
    in the export covers recovery).
  * `/` — substring filter across the comment
    body + author + paragraph breadcrumb
    (case-insensitive).  `Enter` / `Esc`
    exit filter mode without closing the
    modal.
  * `Esc` — close.

  The panel re-loads each sidecar on every
  resolve / delete to avoid clobbering an
  out-of-process edit, and updates the open
  paragraph's in-memory comment cache when the
  edited paragraph is the one currently open
  in the editor (so the inline span overlay
  matches immediately).

* **`inkhaven comments <subcommand>`** CLI:

  | Subcommand | Use |
  |-|-|
  | `list [--paragraph <slug>] [--unresolved-only]` | Table view; UUID printed under each row so `resolve` / `delete` chords are easy to copy. |
  | `resolve <UUID>` | Find the sidecar containing the UUID; mark resolved + write timestamp. |
  | `reopen <UUID>` | Inverse of resolve. |
  | `delete <UUID>` | Drop the comment from its sidecar.  Immediate. |
  | `export [--output <path>]` | Structured JSON dump of every comment with paragraph slug-path + title + comment fields.  Streams to stdout when `--output` omitted. |

  CLI mirrors the panel — every operation
  writes through to the same sidecar files so
  a `Ctrl+V Shift+C` panel session and a
  shell-pipe `inkhaven comments resolve …`
  produce the same on-disk state.

* **Surfaces:**
  * `Action::ViewCommentsPanel` (rebindable).
  * `Modal::CommentsPanel { entries, cursor,
    filter, filter_active, hide_resolved,
    visible }` + `CommentsPanelEntry` struct
    in `src/tui/modal.rs`.
  * `App::open_comments_panel` +
    `collect_all_comments_for_panel` +
    `comments_panel_handle_key` +
    `comments_panel_refilter` +
    `comments_panel_resolve_current` +
    `comments_panel_delete_current` in
    `src/tui/app/comments_impl.rs`.
  * `draw_comments_panel_modal` in
    `render/modals.rs`.
  * `src/cli/comments.rs` — CLI walker +
    sidecar-locating helper +
    `humanise_age` (duplicated from the TUI
    to keep the modules independent).
  * `CommentsCommand` variant +
    `Command::Comments(...)` dispatch in
    `src/cli/mod.rs`.

* **Phase C.2 follow-up (C.2.1) deferred:**
  * **AI digest** — `a` from the panel
    composes a prompt envelope from the
    unresolved comments + the paragraphs they
    anchor to, asks the LLM for a thematic
    summary (structural vs prose vs
    factual).
  * **Cursor-jumps-to-span** on `Enter` —
    open the paragraph and immediately
    position the cursor at `char_start` of
    the comment.
  * **Per-paragraph dense indicator** —
    the `CommentsPanelEntry.paragraph_total_comments`
    field is already populated; the panel
    renderer just needs the `(N/M in ¶)`
    suffix.

610 tests pass — Phase C.2 is mostly walker +
serialise + IO glue that the C.1 unit tests
already exercise indirectly through the
sidecar IO.  Manual TUI testing required for
the panel chord set + CLI execution.

**Phase C complete.**  Inline comments now
have: anchor (C.1), inline span overlay +
resolved dim (C.1.1), project-wide panel +
sidecar IO + CLI (C.2).  Beta-reader review
loops are practical.

### Q.3 — AI continuation + inline footnote

**`Ctrl+V d` AI continuation drafting**
(action: `ai.continuation_draft`).  Asks the
configured LLM to continue the open paragraph
in the author's voice.  Prompt envelope sends:
* The previous N paragraphs (configurable via
  `editor.continuation_anchor_count`, default
  3) as voice anchors, in chronological order
  with their titles.  Walks the hierarchy
  skipping system books so the anchors are
  manuscript prose only.
* The open paragraph's existing text with a
  `[[CURSOR_HERE]]` marker at the cursor
  position so the model knows where to
  continue from.

Response wrapped in `<<<DRAFT>>>` / `<<<END>>>`
markers; the AI pane's `I` apply lifts only
the draft block.  Falls back to inserting the
full response (with a status hint) when the
LLM forgot the markers.  Pairs with
`Ctrl+V y` snippet expansion (`\\tdo` →
`TODO:` + `Ctrl+V d` → AI-generated TODO body).

**`Ctrl+V f` inline footnote** (action:
`editor.insert_footnote`).  Pops a multi-line
text input modal (`Modal::FootnoteEditor`,
mirroring `CommentEditor`).  On `Ctrl+S`
commit, inserts at the cursor:
* `#footnote[<body>]` — Typst, the default.
* `[^id]` + trailing `[^id]: <body>` — when
  `editor.footnote_style = "markdown"`.  ID is
  a fresh 6-char UUID prefix so multiple
  footnotes per paragraph don't collide.

### Q.4 — Project goal modal + style transfer

**`Ctrl+V Shift+G` project goal modal** (action:
`view.project_goal_modal`).  Reads new
`project: { word_count_goal, target_date,
counted_books }` HJSON stanza.  Materialises:
* Total project words (sum of `Node.word_count`
  across user books, filtered by
  `counted_books` when set).
* Percentage of goal + ASCII progress bar.
* Days remaining to `target_date`.
* Words-per-day required from today.
* Per-book breakdown (title + words +
  contribution %).
* Verdict glyph + label:
  `✓ ahead` (projection on/before target),
  `· on track` (within 7 days),
  `✗ behind` (more than 7 days late),
  `✓ complete` (already at goal),
  `— no goal set` /
  `— no target date`.

Recent-average + projection-date wiring queued
for Q.4.1 (hook into the duckdb-backed progress
event log the daily-streak heatmap already
uses).

**`Ctrl+V y` style transfer rewrite** (action:
`ai.style_transfer_rewrite`).  Two-step UX:
1. Pops `Modal::StyleTransferPicker` scoped
   to the current book (every paragraph
   except the open one).  Standard picker
   chords: ↑↓, Enter, `/` filter, Esc.
2. On commit, composes a prompt envelope
   asking the LLM to rewrite the open
   paragraph in the picked reference
   paragraph's style.  Match: sentence
   length, vocabulary register, narrative
   distance, rhythm, mood.  Preserve:
   literal meaning, named entities, plot
   facts.

Response wrapped in `<<<REWRITE>>>` /
`<<<END>>>` markers; AI pane `I` apply extracts
only the rewrite.  Different from `Ctrl+B
Shift+M` rhythm rewrite (which targets rhythm
only with a prompt-defined style); style
transfer targets a CONCRETE EXAMPLE the
author picks.

**New surfaces:**
* `Action::AiContinuationDraft` /
  `EditorInsertFootnote` /
  `ViewProjectGoalModal` /
  `AiStyleTransferRewrite` — all rebindable.
* Modal variants: `FootnoteEditor`,
  `ProjectGoalModal`, `StyleTransferPicker`
  in `src/tui/modal.rs`.
* `App.pending_continuation_draft` +
  `App.pending_style_transfer` flags drive
  the `I`-apply extraction (same single-shot
  pattern as `pending_translation`).
* `src/tui/project_goal.rs` — `ProjectGoalData`
  + `Verdict` + pure verdict-math helper.
* `src/tui/app/q3_q4_impl.rs` — handlers for
  all four chords + prompt composers +
  `extract_continuation_text` /
  `extract_rewrite_text` helpers (shared
  `<<<END>>>` close marker).
* `draw_footnote_editor_modal` /
  `draw_project_goal_modal` /
  `draw_style_transfer_picker_modal` in
  `render/modals.rs`.
* `progress_bar` helper in `render/modals.rs`.
* `EditorConfig` gains
  `continuation_anchor_count: usize` (default
  3) + `footnote_style: String` (default
  `"typst"`).
* `Config` gains `project: ProjectConfig`
  with `word_count_goal: u64`,
  `target_date: String`,
  `counted_books: Vec<String>`.

**Phase Q.3 + Q.4 tests** — 12 new:
* `extract_continuation_block` /
  `extract_rewrite_block` /
  `extract_returns_none_on_missing_markers`
* `mark_cursor_inserts_marker_in_correct_position`
* `parse_iso_date_handles_valid_and_invalid`
* `verdict_no_goal_when_goal_zero` /
  `_complete_when_past_goal` /
  `_no_target_when_target_missing` /
  `_ahead_when_projection_before_target` /
  `_on_track_when_projection_within_seven_days`
  / `_behind_when_projection_far_after_target`
  / `_glyphs_and_labels_are_distinct`

622 tests pass (was 610 — +12).  Zero new
dependencies.

**Phase Q.4.1 deferred** — wire the duckdb-
backed progress event log into
`compute_recent_words_per_day` so the
projection date is real (currently returns
`None` and the modal shows a stub message).

### Phase D — tooling polish

Three follow-up items that round out 1.2.14's
anchor themes.  Mirrors how 1.2.13's Phase D
shipped `language export` + `language doctor`
to round out the Language book.

#### D.1 — Threads export + doctor

* **`inkhaven thread doctor [--json]`** —
  health report for the Threads system book.
  Status distribution + weight distribution +
  average tension + three blind-spot detector
  passes:
  * **ZERO LINKS** — thread status past `setup`
    but the project has no paragraph linking
    to it.
  * **PAYOFF UNFIRED** — status `payoff` but
    no paragraph links to the thread.
  * **DORMANT** — status `develop` but 0-1
    project-wide links (the thread declared
    motion but isn't getting it).

  Exit code 0 always — informational, not a
  pass/fail gate.  `--json` produces structured
  output for CI gates.

* **`inkhaven thread export --format
  <json|csv|markdown> --output <path>`** —
  three formats:
  * `json` (default) — pretty-printed
    structured dump with every schema field.
    Streams to stdout when `--output` omitted.
  * `csv` — flat table; paradigm fields
    (characters / places / artefacts /
    related_threads) collapsed to
    `;`-separated strings.  RFC 4180 quoting.
  * `markdown` — printable summary document;
    one `##` section per thread with arc shape
    + connections + notes.

  Entries sorted by lowercased name for stable
  diffs across runs.

* **Surfaces:**
  * `ThreadCommand::{Doctor, Export}` variants
    + `ThreadExportFormat` value-enum.
  * `cli::thread::{doctor, export}` +
    `ThreadFull` struct + `parse_thread_full`
    + `csv_field` helpers.

* **D.1 tests** — 3 new:
  * `csv_field_quotes_when_needed`
  * `parse_thread_full_extracts_every_field`
  * `parse_thread_full_returns_none_on_empty`

#### D.2 — Project goal projection wiring

Closes the Q.4.1 deferred item.
`App::compute_recent_words_per_day` now reads
the existing `progress_cache.sparkline`
(last-30-day word delta, oldest first,
project-wide).  Averages over NON-ZERO days
so a stretch of vacation doesn't drag the
projection rate down to "you'll never finish"
status.  Returns `None` only when the cache
hasn't built yet OR every day is zero.

The Q.4a project goal modal now shows the real
recent-avg + projection-date instead of the
"Q.4.1 wiring queued" stub line.

#### D.3 — AI comments digest

`a` chord in the `Ctrl+V Shift+C` comments
panel.  Snapshots the visible comments (so the
filter state controls the digest scope),
closes the panel, composes a prompt envelope
asking the LLM to group concerns by category:

* **STRUCTURAL** — pacing, scene order, arc
  shape, what's missing.
* **PROSE** — sentence rhythm, register,
  voice, word choice.
* **FACTUAL** — continuity, character / place
  consistency.
* **QUESTION** — reviewer asking the author
  for clarification.

For each category, the LLM lists the SHORT
theme + references the paragraph breadcrumbs
that surfaced it.  Ends with a one-sentence
overall verdict: dominant concern + most
actionable next pass.

Streams into the AI pane via the standard
`Inference` machinery; no marker extraction
for the digest path — the response is meant
to be read in the pane, not inserted into a
paragraph.

* **Surfaces:**
  * `App::start_comments_digest` +
    `compose_comments_digest_prompt` in
    `src/tui/app/comments_impl.rs`.
  * Panel hint updated to mention `a AI digest`.

625 tests pass (was 622 — +3).  Zero new
dependencies.

**Phase D complete.**  Threads has its full
export+doctor surface; the project goal modal
shows real projection math; the comments panel
has a thematic AI summary one keystroke away.

#### D.4 — TUI thread doctor modal

`Ctrl+V Shift+D` (action: `view.thread_doctor`)
opens a modal that mirrors `inkhaven thread
doctor`'s output entirely inside the TUI:

* Thread count + average tension.
* Status distribution (setup / develop /
  payoff / resolved / abandoned counts).
* Weight distribution (major / subplot /
  runner / bridge counts).
* Three blind-spot detector lists with the
  same thresholds the CLI uses:
  * **ZERO LINKS** — status past `setup` but
    no project paragraph links to it.
  * **PAYOFF UNFIRED** — status `payoff` but
    no project links.
  * **DORMANT** — status `develop` but ≤1
    link project-wide.

Read-only modal; `Esc` or `Enter` close.
Closes the TUI/CLI parity gap on the
Threads surface — every CLI subcommand
(`add` / `list` / `doctor` / `export`) now
has a TUI equivalent: `+` under Threads /
`Ctrl+V Shift+H` picker / `Ctrl+V Shift+D`
doctor / (export stays CLI-only by design —
the format selectors don't fit a modal UX).

Surfaces:
* `Action::ViewThreadDoctor` (rebindable).
* `Modal::ThreadDoctor { data: ThreadDoctorData }`
  with the precomputed snapshot.
* `App::open_thread_doctor` +
  `compute_thread_doctor_data` +
  `thread_doctor_handle_key` in
  `src/tui/app/threads_impl.rs`.
* `draw_thread_doctor_modal` in
  `render/modals.rs`.

#### D.5 — Five Russian-literature project templates

`inkhaven init <path> --template <name>` now
accepts five additional templates that scaffold
genre-authentic Russian-literature project
structures with Russian-language book / chapter
/ seed-paragraph titles.  Each post-init
message recommends setting `language:
"russian"` in `inkhaven.hjson` so the Snowball
stemmer + multilingual prompt resolver flip to
Russian.

**Research notes** (each template's structure
derives from canonical works):

* **`russian-novel`** (русский роман) —
  Tolstoy / Dostoyevsky tradition.  Book
  titled `Рукопись`; chapters `Часть Первая`
  / `Часть Вторая` / `Часть Третья` + `Эпилог`
  (matches the four-volume structure of
  `Война и мир` at smaller scale).
  Characters seeded with `главный герой`,
  `антагонист`, `наперсник` — the three
  canonical roles of the 19th-century
  Russian novel.  Recommended goal 100K
  (`Анна Каренина` is ~360K; `Война и мир`
  ~580K — 100K is the Чехов-novel anchor
  for first-time authors).

* **`russian-long-story`** (русская повесть)
  — Pushkin / Bunin / Gogol tradition.
  Single-arc, 7 numbered chapters in Roman
  numerals (`I` through `VII`) + `Эпилог`.
  Powest doesn't pluralise across multiple
  POVs — pre-seed a single
  `главный герой` paragraph with a note
  about the typical frame-narrative choice
  (рассказчик-свидетель vs first-person).
  Recommended goal 35K (the Бунин-Гоголь
  range is 20K-50K).

* **`russian-scifi`** (русская научная
  фантастика) — Strugatsky / Belyaev /
  Lukyanenko tradition.  Book titled
  `Научная фантастика`; chapters `Пролог`,
  three named parts (`Земля` / `Полёт` /
  `Звёзды`), `Эпилог`, `Глоссарий` (for
  invented-term tracking — a standard
  feature of Russian sci-fi).  Pre-seeds
  Places with `звёздная база` + `колония`
  and Artefacts with `артефакт`.
  Recommended goal 80K (Стругацкие-Лукьяненко
  middle range).

* **`russian-lore`** (русский лор /
  мифология) — былины + structured-
  worldbuilding tradition.  Book titled
  `Лор`; sections by mythological category:
  `Происхождение мира`, `Боги`, `Герои`,
  `Чудовища`, `Мифы и сказания`.  Standalone
  tales, NOT continuous narrative — the
  structure mirrors classic collections
  rather than novels.  Pre-seeds Places
  (`священная гора`), Artefacts
  (`реликвия`), and a Threads paragraph
  (`сотворение мира`) with full HJSON shape
  to anchor cosmogony.  Recommended goal
  50K.

* **`russian-utopia`** (русская утопия) —
  Чернышевский (`Что делать?`) / Богданов
  (`Красная звезда`) tradition.  Book titled
  `Утопия`; frame chapter `Прибытие`
  followed by topic-organised chapters per
  aspect of utopian society (`Труд`,
  `Семья`, `Образование`, `Искусство`,
  `Будущее`).  Pre-seeds `город будущего`
  (Place), `проводник` (Character — the
  classic utopia-tradition guide), and the
  central Thread `принятие утопии` with
  arc-shape ready (`opening` / `midpoint` /
  `payoff` for the standard
  arrival → cost-awareness → choice
  trajectory).  Recommended goal 60K.

**Templates registry** lives in
`src/cli/templates.rs::TEMPLATES`; six
existing + five Russian = 11 total.
`inkhaven template list` shows every
template's description (the table widens to
fit the longest name).

**Phase D.5 tests** — 6 new:
* `russian_templates_all_registered`
* `russian_novel_has_three_parts_plus_epilogue`
* `russian_long_story_uses_roman_numerals`
* `russian_scifi_includes_glossary_and_places_seeds`
* `russian_lore_thread_seed_parses_as_hjson`
* `russian_utopia_thread_seed_parses_as_hjson`
* `russian_utopia_chapters_match_topic_structure`

632 tests pass (was 625 — +7).  Zero new
dependencies.

### Polish round — C.2.1 + Q.2.1 + D.6 + D.7

#### C.2.1 — Comments panel polish

* **Cursor jumps to span on Enter** — opening a
  paragraph from the panel now positions the
  editor cursor at the comment's `char_start`
  via `char_offset_to_row_col` + `CursorMove::Jump`.
  The author lands on the prose the comment
  attaches to instead of the top of the paragraph.
* **Per-paragraph dense indicator** — panel rows
  now show `(N/M in ¶)` when a paragraph carries
  more than one comment.  `CommentsPanelEntry`
  gains a `paragraph_position` field tracking the
  1-indexed position; single-comment paragraphs
  suppress the indicator (avoiding noise).

#### Q.2.1 — `{cursor}` placeholder in snippets

Snippets can now contain `{cursor}` to control
where the editor cursor lands after expansion.
The placeholder passes through
`expand_placeholders` verbatim (as an unknown
placeholder); the editor's expansion driver
splits the result at the first marker, pastes
the head, captures the cursor position, pastes
the tail, then moves the cursor back to the
captured position.

Snippets without `{cursor}` paste atomically
(cursor ends after the last char, unchanged
from Q.2 behaviour).

Picker-based placeholders (`{char_lookup}` /
`{place_lookup}` / `{artefact_lookup}`) and the
`bund:` prefix for Bund-VM expansion are still
queued — both need a modal state machine the
synchronous snippet pipeline doesn't yet have.

1 new test: `expand_cursor_placeholder_passes_through`.

#### D.6 — More genre templates

Three additional `inkhaven init --template <name>`
options round out the genre coverage:

* **`epic-fantasy`** — Tolkien / Sanderson / Jordan
  tradition.  Book scaffold: Prologue + three named
  Books (Homeland / Road / Tower) + Epilogue + 3
  Appendices (Dramatis Personae / Glossary / Maps).
  Pre-seeds Characters (hero / shadow / mentor /
  herald — Joseph Campbell archetypes), Places
  (the homeland / the wilds / the dark tower),
  Artefacts (the artefact / the mentor's gift),
  and three Threads spanning the call /
  descent / return arcs.  Recommended goal 150K
  (volume one of a trilogy).

* **`mystery`** — Christie / Doyle / procedural
  tradition.  Crime-investigation-misdirection-
  solution structure.  Pre-seeds Characters (the
  detective + the victim + three suspects) and
  three Threads (the crime / the misdirection /
  the solution) — every thread's role in classic
  mystery construction.  Recommended goal 70K.

* **`french-novel`** — Hugo / Flaubert / Camus
  tradition.  `Manuscrit` book with `Première
  partie` / `Deuxième partie` / `Troisième partie`
  + `Épilogue`.  Pre-seeds Characters in French
  (`protagoniste` / `antagoniste` / `confident`).
  Post-init message recommends `language:
  "french"` in inkhaven.hjson for the Snowball
  stemmer + multilingual prompt resolver.
  Recommended goal 90K.

Template registry now totals 14 (6 original +
5 Russian + 3 here).  4 new tests pin the
schema:
* `epic_fantasy_has_full_scaffolding`
* `mystery_threads_parse_as_valid_hjson`
* `mystery_seeds_three_suspects`
* `french_novel_uses_french_partition_names`

#### D.7 — Strip phase markers from user-visible output

Audit pass to remove `1.2.14+ Phase X.Y — ` and
similar version+phase prefixes from anything that
ends up in `--help`, `F1` quick-ref, status bar
messages, or user-facing documentation prose.

Stripped from:
* CLI doc comments on clap-derive variants in
  `src/cli/{mod,thread,language,comments,templates,
  init}.rs` (drive `--help` text).
* Keybind action labels + descriptions in
  `src/tui/keybind.rs` (drive F1 quickref).
* Status messages with inline phase mentions.
* Tutorial prose in `Documentation/Tutorials/`.

Kept intact (per the user's direction):
* Doc comments + `//` comments inside source code
  (developer-facing, not user-visible).
* Release notes (`Documentation/RELEASE_NOTES/`)
  and proposal docs (`Documentation/PROPOSALS/`)
  — those are version-historical by design.
* Terse `(1.2.13+)` / `(1.2.14+)` version markers
  inline in reference docs and action descriptions
  — they tell users WHICH version added a feature
  without phase jargon.

637 tests pass (was 632 — +5 across C.2.1, Q.2.1,
D.6).  Zero new dependencies.

## Closing

1.2.14 shipped 9 phases of planned work (A.1-A.3,
C.1 + C.1.1 + C.2 + C.2.1, Q.1, Q.2 + Q.2.1, Q.3,
Q.4) plus a 7-step tooling polish round (D.1-D.7)
that wasn't in the proposal but landed cleanly
alongside.

Tests went 565 → 637 (+72), zero new dependencies,
one new top-level system book (`Threads`), three
new CLI surfaces (`thread`, `comments`, `template`),
14 init templates registered.

Deferred to 1.2.15: the picker-based snippet
placeholders (`{char_lookup}` / `{place_lookup}` /
`{artefact_lookup}`) and the `bund:` prefix —
both need an async snippet state machine that
this cycle's synchronous pipeline doesn't yet
have.  Plus the Language-book carryovers
documented above (`--format grammar` /
`phrasebook` / `csv` exports + `Ctrl+B Shift+R`
reverse-lookup + word-of-the-day card).
