# Inkhaven 1.2.23 — Stability & fixes

The last 1.2 housekeeping pass before the 1.3 *Editorial Pass* cycle. No
new headline feature — a crash-behaviour fix, a whole-tree panic-surface
re-audit, SQL hardening across the storage layer, the deferred find &
replace polish, and one consolidation. The goal: a clean, calm base for
1.3.

Tests 1186 → 1190. Zero new Rust dependencies.

## Broken pipe no longer writes a crash report

Piping inkhaven's stdout into a truncating reader (`inkhaven … | head`)
made the print macros panic with `Broken pipe (os error 32)` — Rust
ignores SIGPIPE — and the crash hook turned that normal shell event into
a spurious `inkhaven-crash-<UTC>.hjson` in the working directory.

Fixed in the panic hook with a **Step 0** that detects the print-macro
broken-pipe panic (`is_broken_pipe_panic` — narrow: requires the
`failed printing to std…` prefix *and* a broken-pipe error, so a disk-full
or any other output failure still surfaces a report), restores the
terminal, and exits cleanly. No SIGPIPE reset — that would risk killing
the long-running TUI on any internal/subprocess pipe close — so the editor
is untouched. Verified: `inkhaven … --dry-run | head` exits 0 with no
crash file.

## Panic-surface re-audit

Four parallel auditors swept the whole tree. It is broadly clean to the
1.2.15 bar — no reachable `panic!`/`unreachable!`, no lock-poison
surfaces, no production `.unwrap()`/`.expect()` on user-data paths. The
real findings clustered into one recurring class — **UTF-8 byte-boundary
slicing using an offset taken from a lowercased/transformed copy** — plus
a write-durability item. All fixed:

- **`config::parse_color`** — a multibyte char in a 3- or 6-byte hex
  theme value (e.g. `"#aé"`) split a UTF-8 char and panicked, reachable
  from *any* HJSON theme field. Now gated with `is_ascii()` (regression
  test added).
- **`jump_cursor_to_text`** — a `to_lowercase()` byte offset sliced the
  original line; non-English folds (Turkish İ, ẞ, ligatures) before the
  match panicked. Reachable via fact-check navigation. Fixed by slicing
  the lowercased string.
- **`doctor_scan`** naming scan — same class: a lowercased offset sliced
  the original prose. Reachable via `inkhaven doctor` on multilingual
  prose. Fixed with an `is_char_boundary` guard.
- **`store/mod.rs`** — four `std::fs::write` sites (paragraph/script
  templates, seeded body, image bytes) routed through `io_atomic::write`
  for torn-write safety, matching the house standard.

## SQL hardening

The storage layer is now free of string-interpolated SQL. The JSON +
blob stores (`storage/engine.rs`) and the progress store
(`progress/store.rs`) migrated their `format!` + `sql_escape` SELECTs onto
bound parameters via a new `select_all_with`; the JSON document binds as a
parameter and casts in-SQL (`CAST(? AS JSON)`). Both copies of
`sql_escape` are removed. Never an injection vector before (the only
interpolated value was serde-serialized JSON), but the binding approach is
robust by construction. A new adversarial round-trip test stores a
document full of quotes, backslashes, and a `'; DROP TABLE …` string and
confirms it round-trips intact with the table whole.

## Find & replace polish

The project-replace review modal (1.2.22) now lets you change the matching
mode in place: **`w`** toggles whole-word, **`i`** ignore-case, **`x`**
regex — each re-runs the scan, and the header shows the active mode. So
the editor reaches the same regex / substring / case modes the CLI's
`inkhaven replace` flags do, starting from the safe whole-word default.

## Consolidation

`continuity`, `tension`, and `facts_scan` each carried a byte-identical
manuscript walk; the triplicate is now one `Hierarchy::user_book_chapters()`
method (net-negative LOC). Direct 1.3 prep — the Editorial Pass iterates
exactly this walk.

## Carried forward to 1.3

- **M.1** — cap the crash-report action-ring `detail` length (the last
  open 1.2.15 stability item).
- The **find & replace modal live click-through** (engine + key wiring are
  unit/CLI-verified; only the on-screen interaction is unconfirmed).
- Low-priority stability items reported by the audit: `backup`/`config`
  non-atomic sentinel saves, `backup::restore` per-entry atomicity.
- Older carryovers: ePub inline images + popup footnotes; the 1.2.19
  continuity/anachronism follow-ups; audiobook polish.
