# Inkhaven 1.2.19 — Revision & continuity (multilingual)

**Active scope locked 2026-06-03.**  This cycle builds
the revision layer — the draft-3-to-final grind — and
builds it multilingual from the start:

* **C.1** echo / repetition-at-distance detector
* **C.2** continuity lexicon + numeric/temporal/spatial
  contradictions (closes A.5.b)
* **C.3** continuity bible / character fact extraction
  (headline)
* **C.4** unresolved-tension detector (closes A.6.b)
* **X.1** standard manuscript-format export
* **D.1** docs

See `Documentation/PROPOSALS/1.2.19_PLAN.md` for the
three-tier multilingual model (Tier 1 AI prompts / Tier 2
Snowball+stop-words / Tier 3 quantity lexicons), the
phase plan, test budget, and risk register.

The remainder of this file is the carryover backlog —
themes from prior cycles outside the 1.2.19 shape.  (A.5.b
+ A.6.b are now folded into the active scope above.)

## Carryovers from 1.2.18

### R.1.b — ePub polish

R.1 shipped a valid, well-formed EPUB 3.  Polish
deferred:

* **Cover image** — embed a cover (from a
  configured path or the book's first Image
  node) as the EPUB cover + nav landmark.
* **Inline images** — carry Image nodes into the
  chapter XHTML (`<img>` + the blob) instead of
  skipping them.
* **Popup footnotes** — render `#footnote[…]` as
  proper EPUB 3 popup endnotes (`epub:type="noteref"`
  + a notes section) instead of the inline span.
* **`--paragraph-headings`** opt-in to surface
  paragraph titles as scene headers for authors
  who use them deliberately.

### R.2.b — Audiobook polish

* **Parallel synthesis** — synthesise chapters
  concurrently (CPU-bound), capped at core count,
  to cut the roughly-real-time wall-clock.
* **Cover art** — embed the cover image into the
  `.m4b` metadata.
* **Configurable bitrate** — `--bitrate` flag
  (default 64k).

### R.3.b — `paragraph-too-long` doctor class

The optional doctor scan class flagged in the
R.3 plan: a paragraph whose read time exceeds a
threshold (e.g. 5 min at `reading_wpm`).  Info
severity, no autofix — an author-judgment
finding like the 1.2.16 narrative-audit classes.

### I.1.6 — DuckDB write batching

Deferred from the I.1 perf pass per the data:
it's the save path, not profiled by the read-path
`_bench-load`.  Needs a `_bench-save` companion
instrument first, then coalesce per-save DuckDB
commits with a window.  Smaller win than the
I.1.4/I.1.5 algorithmic fixes; revisit if a
save-heavy workload shows up in profiling.

### I.1.2.b — In-process bench scenarios

The bench harness ships subprocess-style
`startup` + `search`.  Three scenarios from the
plan need in-process access (a subprocess
spawn's ~85 ms swamps a 0.05 ms flatten):

* `tree_scroll` — the now-O(n) flatten.
* `ai_envelope` — RAG retrieval + prompt build.
* `paragraph_save` — full save round-trip.

These land alongside a lib+bin crate split (a
larger refactor) or a hidden `_bench <op>`
subcommand per scenario.

### I.1.1.b — Fixture system-book population

`gen-fixture` ships the user-book backbone.
Deferred: Characters / Places / Threads
population, inline comments, tags — improves
bench realism without blocking the harness.  Also
an optional `--no-embed` flag so startup / tree
scenarios can skip the slow embedding step.

## Carryovers from 1.2.17

### T.4.b — Voice integrity verification (MD5)

The catalog exposes `md5_digest`; T.4 ships voice
auto-download without verifying it.  Add a
subprocess `md5sum` / `md5 -q` / `certutil` check
after download.

### T.6.b — Background voice downloads

The `Ctrl+B Shift+V` picker blocks the UI during
a voice download.  Lift to a tokio task + a
status-bar progress chip (the 1.2.15 health-
monitor pattern).  Note: the R.2 audiobook export
uses blocking synth by design (batch job), so
this is a TUI-picker concern only.

### Apple Silicon Piper

Upstream packaging bug — `piper_macos_aarch64.tar.gz`
ships x86_64.  Track piper's GitHub for a real
arm64 build; flip the macOS `auto` default when
it lands.

## Carryovers from 1.2.16

### A.2.b — Dashboard sections

POV chip, rhythm gauge averaged across the
project, filter-words flagged today, recent AI
critique — each adds one row to the `Ctrl+V
Shift+J` modal.

### A.6.b — Unresolved-tension detector

The fourth plot-mining class; heuristic needs
real-manuscript calibration.

### A.5.b — Worldbuilding pack additions

Numeric / spatial contradictions; anachronism
overlay.

### I.4.b/c — Bund stdlib follow-ups

`ink.thread.add_waypoint`, `ink.thread.link_paragraph`,
`ink.workflow.chain`.

## Carryovers from 1.2.15

### Phase G remainder

* Uncommitted-changes warning at TUI exit.
* Disk-space pre-flight (needs `fs2` / `sysinfo`).

### M1 / M3 — security hygiene

* Crash-report action-ring detail audit.
* `[script: <slug>]` prefix on script-opened
  modals.

## Carryovers from 1.2.13

### Language-book UX polish

Reverse-lookup picker, word-of-the-day card,
Dictionary / Grammar / Phonology card renderers,
inline gloss-table renderer, confidence-flag chip,
inflection-aware status chip.

## Themes under consideration

### AI maturity

* **Multi-paragraph envelope** — give the AI ±1
  paragraph so it stops producing abrupt openings
  / non-sequitur endings.
* **Character voice consistency** — pick a
  character + a chapter, ask "does X speak in
  their voice here?" against the Characters book.
* **Hot-reload `prompts.hjson`** — watch the
  mtime, reload on change.
* **AI streaming into the editor** — stream at
  the cursor (vs the AI pane), Esc cancels +
  undoes.

### Genre packs

Mystery + historical-fiction project templates
with style-rule packs (clue tracker, fair-play
audit, anachronism overlay co-shipping with
A.5.b).

### Reader experience (continued)

* **Reading-time doctor** — flag chapters whose
  read time is a wild outlier (pairs with R.3.b).
* **Per-character audiobook voices** — map a
  Piper voice per Characters entry so dialogue
  routes to that voice.

### Other candidates

* **Sixth supported language** — Italian /
  Portuguese / Japanese.
* **GitHub Release object backfill** for
  1.2.11–1.2.18.

## Implementation log

The revision layer — four detectors + a submission
export — built multilingual from the start across the
three-tier model the plan lays out.

### C.1 — Echo / repetition-at-distance detector

**Tier 2 (stemmer/stop-word).**  `src/echo.rs` —
`detect_echoes` tokenises (UAX-#29), stems
(`parse_stemmer_language`, 18 languages), drops
stop-words (`built_in_stop_words`), and flags a
distinctive stem reused `min_repeats` times within an
N-paragraph window.  "Distinctive" is corpus-relative —
the stem must repeat but stay under `max_global` total
count, so common vocabulary isn't flagged — which needs
no per-language frequency tables.  Wired as the
`echo-repetition` doctor class (Info, no autofix), per
chapter.  Config: `editor.echo_window` /
`echo_min_repeats` / `echo_max_global`.

A real multilingual lesson surfaced here: the Russian
Snowball stemmer leaves `ё` unstemmed (needed a `ё`→`е`
fold), *and* it's a stemmer not a lemmatiser — noun
declension collapses (`корабль`/`корабля`/`корабле`) but
some irregular verb past-tense forms don't.  Documented;
fixtures use noun cases.  12 tests.

### C.2 — Continuity lexicon + numeric/temporal/spatial contradictions

**Tier 3 (lexicon — the genuine new per-language work).**
Closes A.5.b's numeric/spatial half.  `src/continuity.rs`
— a `ContinuityLexicon` (number-words + scales + temporal
units→seconds + spatial units→metres + directions),
bundled for **English / French / Spanish**;
`extract_quantities` parses digits, number-words,
hyphenated compounds (`twenty-five`), and `<number>
<scale>` pairs into normalised `Quantity` values;
`detect_contradictions` runs two narrow checks — spatial
direction reversal (opposite cardinal + equal magnitude
in a window) and proximate temporal mismatch (different
durations same/adjacent sentence).

Directions are only extracted attached to a magnitude,
never bare — sidestepping the Spanish `este` = east/this
ambiguity *and* the "walked north then south" false
positive.  Languages without a bundled lexicon (Russian,
German, …) skip cleanly; the `bootstrap-continuity`
seed CLI is a follow-up.  Wired as `numeric-contradiction`
(Info, no autofix).  18 tests.

### C.3 — Continuity bible (the headline)

**Tier 1 (AI) + Tier 2 (stemmer comparison).**
`src/continuity_bible.rs` + `src/cli/continuity.rs` —
`inkhaven continuity extract` runs an AI pass
(per-language prompt) recording each character's
established facts to `<project>/.inkhaven/continuity.json`;
`detect_drift` groups facts by (character, attribute) and
flags attributes whose stemmer-normalised values disagree
across chapters; `continuity-drift` doctor class surfaces
them.  `inkhaven continuity list` dumps the bible.

The stemmer comparison means inflected restatements
("green eyes" / "green eye", ru `зелёные` / `зелёными`)
don't false-flag while genuine drift (green vs brown,
`зелёные` vs `карие`) does.  Embedding-based synonym-aware
comparison ("green" vs "emerald", cross-language) is a
C.3.b refinement; within a single-language manuscript —
the common case — the stemmer comparison is sound.  14
tests (the live AI call is the only manual-verify
boundary; prompt + parse are tested).

### C.4 — Unresolved-tension detector (opt-in)

**Tier 1 (AI) + Tier 2.**  Closes A.6.b.
`src/tension.rs` + `src/cli/tension.rs` —
`inkhaven tension scan` tags each chapter's introduced +
resolved tensions to `<project>/.inkhaven/tensions.json`;
`detect_unresolved` flags an introduced tension when no
downstream resolution (chapter index ≥ it) shares a
significant stem with its topic.  The matcher is
*generous* about counting a tension resolved → biased
toward fewer false "unresolved" flags.

This scan is **opt-in** — the new `ScanClass::is_opt_in()`
makes the default-run guard
(`selected.map_or(!c.is_opt_in(), …)`) skip
`unresolved-tension` unless explicitly `--class`-selected,
because tension is a judgment call and the AI tagging is
approximate.  13 tests.

### X.1 — Standard manuscript-format export

`src/manuscript.rs` + `src/cli/manuscript.rs` —
`inkhaven manuscript` emits a submission-ready
**Shunn standard manuscript format** typst document:
12pt Courier double-spaced, 1in margins, a title page
(contact top-left, rounded word count top-right, title a
third down), a running `Surname / KEYWORD / page` header
from page 2, chapters on fresh pages, scene breaks as a
centred `#`, a `# # #` end marker, prose typst-escaped.
`round_word_count` does the Shunn rounding (nearest 100
under 25k, nearest 1000 for novels); `header_keyword`
derives the header word.

Verified end-to-end: the exported `.typ` compiles to a
valid PDF with typst — proof the format is structurally
correct, not just string-matched.  11 tests.

### Closing

The whole revision-&-continuity feature set shipped:
four multilingual detectors (echo, numeric/temporal/
spatial contradictions, continuity bible, unresolved
tension) + the Shunn manuscript export.  Each detector
lands on a different multilingual tier — stemmer, lexicon,
AI — exactly as the three-tier plan designed, with no
feature English-only: each either works across all 18
Snowball languages, rides the per-language prompt
resolver, or degrades gracefully with a clear path to
full coverage.

A.5.b (numeric/spatial) + A.6.b (unresolved tension) are
now closed.

Tests grew from 1053 (cycle open) to 1121 — net +159
across the cycle.  No new Rust dependencies; the
detectors reuse the existing Snowball / stop-word /
whatlang / AI / typst stack.

Deferred (follow-ups, non-blocking):

  * **C.1.b** — the live editor overlay for echoes
    (pairing with `Ctrl+B Shift+F`).
  * **C.2.b** — Russian + German quantity lexicons + the
    `bootstrap-continuity` LLM-seed CLI + full
    compound-number parsing.
  * **C.3.b** — embedding-based synonym-aware drift
    comparison + a TUI continuity-bible view.
  * **Anachronism overlay** — the other half of A.5.b
    (period-vocabulary lexicons); a different beast,
    pairs with a future genre-packs cycle.
