# Inkhaven 1.2.18 — Performance pass + reader-experience

**Active scope locked 2026-06-01.**  Two cycle themes:

1. **I.1 performance pass** (deferred from 1.2.16 +
   1.2.17) — fixture generator, criterion-based bench
   harness, flamegraph-driven hotspot fixes, CI gates.
2. **Reader-experience** — ePub export, audiobook
   draft (.m4b), reading-time chip, reader-pace
   preview.

I.1 first.  See `Documentation/PROPOSALS/1.2.18_PLAN.md`
for the eight-decision design doc, on-disk layout,
phase plan (I.1.1–I.1.7 + R.1–R.4), test budget, and
risk register.

The remainder of this file is the carryover backlog —
themes from prior cycles that don't fit the 1.2.18
shape.

## Carryovers from 1.2.17

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

T.4 ships voice auto-download without checksum
verification — the catalog exposes
`CatalogFile::md5_digest` but we don't enforce
it.  ONNX runtime + HTTPS provide reasonable
trust for cycle 1; T.4.b adds the explicit
verification step.

Open question: which hashing primitive?

* Subprocess `md5sum` (Linux) / `md5 -q`
  (macOS) / `certutil -hashfile` (Windows) —
  matches the curl + tar subprocess pattern.
  Per-platform glue.
* Add `md-5` direct dep — already transitively
  in the lockfile (~10 KB compiled).  Cleanest
  ergonomic API.
* Hand-roll MD5 in ~150 lines.  Avoids the
  dep + the subprocess fork.

Recommendation: subprocess.  Matches the rest
of the inkhaven shell-out pattern; deferred to
T.4.b implementation phase.

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

Current `Enter` on an `⬇ available` row in the
voice picker (`Ctrl+B Shift+V`) blocks the UI
for the duration of the download (~5–30 s on a
fast connection).  Lift to a tokio task with a
status-bar progress chip — same pattern as the
1.2.15 health monitor.

Concrete shape:

* Spawn a tokio task that runs
  `ensure_voice_downloaded` with a progress
  callback that posts to an mpsc channel.
* Main loop polls the channel on each
  render frame; status-bar chip shows
  `[tts: en_US-lessac-medium 73%]` (Magenta
  on DarkGray, reusing the 1.2.16 P.1
  `ChipState::Info` glyph).
* Picker stays open during download; voice
  row's chip flips from `⬇ available` to
  `↓ downloading 73%` to `✓ downloaded`.
* Esc during a download cancels (best-
  effort kill of curl, partial files
  cleaned up).

### T.8.b — `inkhaven config` migration prompt

When the schema-aware HJSON editor opens a
project with `tts.voice` matching a `say`-style
name (Milena, Samantha, Alex, etc.) AND
`tts.engine = "auto"`, surface a one-time
prompt: "Piper has no voice named X — pick
`en_US-lessac-medium` (en) / `ru_RU-irina-medium`
(ru) / ..."  Don't auto-overwrite; user
dismisses to keep the existing value.

Deferred because the schema-editor integration
is non-trivial AND the Apple Silicon Piper
limitation means premature migration prompts
on Mac would mislead users.  Revisit once
upstream Piper publishes a real arm64 build.

### Apple Silicon Piper

Upstream packaging bug — `piper_macos_aarch64.tar.gz`
ships x86_64 code (verified with `file` on the
extracted binary).  Track piper's GitHub
issues for a real arm64 build and update the
voice picker's "engine" default on macOS once
it lands.

Workarounds documented in
[Tutorial 56](Tutorials/56-tts-piper.md).

## Carryovers from 1.2.16

### I.1 — Performance pass

The 1.2.16 proposal scheduled `cargo flamegraph`
against a 10K-paragraph fixture + targeted
fixes (lazy tree row materialisation,
background embedding refresh, DuckDB write
batching).  Deferred through 1.2.17 to keep
TTS focused.

Concrete first steps:

* Fixture: build a synthetic 10K-paragraph
  project (script in `tools/`) with realistic
  prose distribution + thread + comment
  density.
* Baseline: capture startup time, first-render
  time, tree pane scroll cost, search round-
  trip, AI envelope assembly time.
* Profile: `cargo flamegraph` per scenario.
* Fix: one PR per hotspot, each tied to a
  benchmark threshold that fails CI on
  regression.

### A.2.b — Dashboard sections

* POV chip on the journal modal.
* Rhythm gauge averaged across the project.
* Filter-words flagged today.
* Recent AI critique (first ~120 chars).

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

The fourth A.6 plot-mining class.  Heuristic
needs real-manuscript calibration before
shipping.

### 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 — Git / backup safety rails (remainder)

* Uncommitted-changes warning at TUI exit.
* Disk-space pre-flight (needs `fs2` or
  `sysinfo` dep — same blocker as 1.2.16 P.1).

### M1 — Crash-report action-ring detail hygiene

Audit every `crash::context().push_action(...)`
call site; document + enforce the rule that
`detail` carries structural metadata only.

### M3 — Script-initiated `ink.input` modal prefix

Prefix Bund-script-opened modals with
`[script: <slug>]` so users can distinguish
them from inkhaven-internal prompts.

## Carryovers from 1.2.13

### Language-book UX polish

Still open from the 1.2.13 backlog:

* `Ctrl+B Shift+R` reverse-lookup picker.
* `Ctrl+B Shift+W` word-of-the-day card +
  phonotactic generator.
* Card renderers for Dictionary / Grammar /
  Phonology paragraphs.
* Inline gloss-table renderer in the AI
  pane.
* Confidence-flag chip (LLM "dictionary
  missing for X").
* Inflection-aware status-bar chip.

## Themes under consideration

### Reader-experience surface

Inkhaven exposes the manuscript-as-data well;
1.2.18 could be the cycle that turns it into
something a reader experiences.

* **ePub export** alongside the typst PDF
  pipeline.
* **Audiobook draft (.m4b)** — Piper TTS +
  chapter slicing.  Falls out of 1.2.17's
  TTS work near-free.
* **Reading-time chip** per chapter / book
  at a configurable wpm.
* **Reader-pace preview** — slow-scroll
  render at the configured wpm.

### Genre packs

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

### TTS Piper polish

* Real-time progress chip during voice
  download (T.6.b).
* MD5 verification on voice files (T.4.b).
* TUI-side picker rewrites `inkhaven.hjson`
  on commit (T.8.b's flip side).
* Track upstream Piper for a real arm64
  release; flip the macOS auto default
  when it lands.

### Other candidates

* **Sixth language for the supported set**
  — Italian / Portuguese / Japanese.
* **GitHub Release object follow-up** —
  backfill Release UI objects for
  1.2.11–1.2.17.

## Implementation log

### I.1 — Performance pass

The performance debt deferred from 1.2.16 + 1.2.17.
Method: build a deterministic fixture, measure,
profile, fix the hotspots the data points at, gate
against regression.

**I.1.1 — Fixture generator.**  Hidden `inkhaven
gen-fixture <path>` produces a deterministic synthetic
project (default 5 books × 20 chapters × 100
paragraphs = 10K paragraphs).  19-line xorshift PRNG +
a 90-sentence hand-curated prose pool with recurring
named entities — vocabulary-realistic enough to
exercise tokenisation, Snowball stemming, embeddings,
and concordance.  Mirrors the TUI save contract
(`io_atomic::write` to disk, then `update_paragraph_content`
for the bdslib copy + re-embed).

**I.1.2 — Criterion bench harness.**  `benches/`
(criterion dev-dep) with subprocess-style `startup` +
`search` scenarios driven against a gen-fixture
project via `INKHAVEN_BENCH_FIXTURE`.  Shared helpers
in `benches/common.rs`; full how-to in
`benches/README.md`.

**I.1.3 — Hotspot analysis.**  A true sampling
flamegraph needs dtrace, which macOS SIP blocks
without sudo — so I.1.3 used timing-instrumented phase
profiling instead (env-gated `INKHAVEN_PERF_TRACE`
checkpoints + a hidden `inkhaven _bench-load`).  The
data **overturned the proposal's hypotheses** and
redirected the fixes.  Two findings dominated, written
up in `Documentation/PROPOSALS/PERF.md`:

  1. **Eager embedding-engine load = 470 ms = 92 % of
     cold start**, project-size-independent.
     `Store::open` built the fastembed model
     unconditionally, even for `inkhaven list`.
  2. **`Hierarchy::flatten` was O(n²)** —
     `children_of` did a full scan per node.  18×
     nodes → 318× time ≈ 18².  Projected ~760 ms/call
     at 10K, on the tree-render path.

  (The proposal's feared `hierarchy.load` cost was a
  non-issue at 6.8 ms; ruled out by measurement.)

**I.1.4 — Lazy embedding-engine init.**
`EmbeddingEngine::new` now stores its construction
params + defers the ONNX load to the first `embed` /
`embed_batch`.  Measured, warm, 2046-node fixture:
`store.open.embedding_engine` 469.62 ms → 0.03 ms;
`total_cold_load` 507 ms → 36 ms; `inkhaven list`
subprocess 486 ms → 85.6 ms.

**I.1.5 — parent→children index.**  `Hierarchy` builds
a `HashMap<Option<Uuid>, Vec<Uuid>>` once in `load`
(O(n) pass over the already-sorted order vec, buckets
pre-sorted with no per-bucket sort).  `children_of`
becomes an O(k) lookup; `flatten` collapses to O(n).
`flatten` 31.98 ms → 0.05 ms (640×) at 2k; projected
~0.25 ms vs ~760 ms at 10K.  `has_children` /
`next_order` / `collect_subtree` / `find_by_path` all
faster for free.  No invalidation logic — `Hierarchy`
is immutable after construction.

**I.1.6 — DuckDB write batching.**  Deferred per the
data: it's the save path (not profiled by the
read-path `_bench-load`), a smaller win than #1/#2,
and wants its own `_bench-save` instrument first.

**I.1.7 — CI regression gate.**  `inkhaven
_bench-report` (15 unit tests) compares two criterion
trees, emits a markdown delta table, exits 2 past a
20 % regression.  `.github/workflows/bench.yml`
publishes a `bench-baseline` artifact on main + fails
PRs on regression.  The threshold is deliberately
loose — the regressions worth gating (re-eager-load,
re-O(n²)) are order-of-magnitude, robust to
shared-runner noise.

### R — Reader experience

Turning the manuscript into something a reader
consumes, not just a writer edits.

**R.1 — ePub export.**  `inkhaven epub` builds a
standards-compliant EPUB 3 container directly via the
in-tree `zip` crate + an in-house typst → XHTML
converter — no pandoc, zero new dependencies.  One
XHTML per chapter, `content.opf` + `nav.xhtml` +
`toc.ncx` + stylesheet.  The converter handles
headings, `_emph_`, `*strong*`, `#footnote[…]`, and
blank-line blocks; paragraph org-titles + chapter
prefixes are stripped for clean reading.  Verified:
every XML member parses well-formed, mimetype is the
stored first entry.

**R.2 — Audiobook (.m4b).**  `inkhaven audiobook`
synthesises a chapter-marked `.m4b` — per-chapter TTS
via the 1.2.17 engine, `ffprobe` for durations, an
ffmpeg metadata file accumulating chapter timestamps,
`ffmpeg` for the concat + AAC mux.  Added
`TtsEngine::audio_extension` (macOS `say -o` rejects
`.wav` + wants `.aiff`; Piper writes `.wav`) — caught
in end-to-end testing.  Verified with ffmpeg 8.1.1: a
real `.m4b` with two correctly-timed chapter markers,
AAC audio, MP4 container.  ffmpeg + ffprobe are
documented required externals.

**R.3 — Reading-time chip.**  `editor.reading_time_chip`
(default off) shows a status-bar `📖 <remaining> /
<total>` chip for the current book at
`editor.reading_wpm` (default 200) — remaining counts
from the open paragraph to the book's end.  Cheap O(n)
walk (the I.1.5 index keeps it linear).

**R.4 — Reader-pace preview.**  `Ctrl+B Shift+E` opens
a teleprompter that advances a word-by-word highlight
through the open paragraph at `reading_wpm`, so the
author experiences pacing at a reader's speed.  The
highlight position is computed from elapsed time each
frame (robust to tick granularity); the main loop
fast-ticks at 40 ms while playing, 200 ms when paused.
Space pause/resume (elapsed carries across cycles),
←→ step, r restart, Esc close.

### Closing

Both halves of the locked scope shipped: the I.1
performance pass (I.1.1–I.1.7, two algorithmic wins
that took cold start 507 ms → 36 ms and `flatten`
O(n²) → O(n)) and the reader-experience headline
(R.1–R.4: ePub, audiobook, reading-time chip,
reader-pace preview).

Zero new Rust dependencies in the reader-experience
work — the `zip` crate was already in-tree for typst
packages, and ffmpeg / curl / tar are the universal
subprocess primitives inkhaven already leans on.  The
perf pass added `criterion` as a dev-dep only.

Tests grew from 962 (cycle open) to 1053.  No
regressions; the new CI bench gate guards the two
algorithmic wins against a silent re-introduction.

Carried forward (side items, non-blocking): T.4.b MD5
voice verification, T.6.b background voice downloads,
plus the larger carryover backlog above.  The
audiobook export uses blocking synth by design — it's
a batch job, so T.6.b's non-blocking concern doesn't
apply to it.
