# Inkhaven 1.2.15 — Stability + Self-Healing

1.2.14 was the **plot intelligence + inline
comments + quick wins** release — a new `Threads`
system book with swim-lane weave view (`Ctrl+V
Shift+H`), AI thread audit (`Shift+A`), and
deterministic thread doctor (`Shift+D`); inline
sidecar comments with span anchoring + project-
wide panel + AI digest (`Ctrl+V c` / `Shift+C`);
14 init templates including 5 Russian and 3
international new (epic-fantasy / mystery /
french-novel); HJSON snippet expansion with 8
placeholders incl. `{cursor}` split-paste; AI
continuation drafting (`Ctrl+V d`); inline
footnote insertion (`Ctrl+V f`); project word-
count goal modal with 30-day projection
(`Ctrl+V Shift+G`); style-transfer rewrite
(`Ctrl+V y`).  Three new CLI surfaces: `thread`,
`comments`, `template`.  Tests 565 → 637 (+72).

**1.2.15 is a stability release.**  Four themes
land:

* **Theme S — Stability sweep.**  Catalogue
  every panic site (`.unwrap()` / `.expect()` /
  `panic!()` / `unreachable!()` / index-into-
  slice / poisoned-lock unwrap) in hot paths;
  replace with `Result` plumbing or graceful
  degradation.  Five sub-phases by module group.
* **Theme H — Background health monitor.**
  Tokio task running periodic integrity checks
  on the DB / Tantivy index / HNSW vectors /
  textarea-vs-disk sync / tree parent-pointer
  integrity.  Status-bar `health` chip.  Safe
  auto-repair (idempotent, never touches user
  content) + per-class HJSON knobs.
* **Theme D — Doctor scan + autofix.**  Project-
  wide problem catalogue (zero-byte files,
  orphan paragraphs, corrupt sidecars, dangling
  wiki-links, orphan thread waypoints, DB/disk
  hash mismatches).  `inkhaven doctor --autofix`
  with per-class confirmation; new TUI modal
  (`Ctrl+B Shift+?` or `Shift+0`).
* **Theme R — Crash report writer + recovery.**
  Custom panic hook flushes dirty buffers to
  `.inkhaven-rescue` files, dumps panic
  context + project state + recent action ring
  to `inkhaven-crash-<ts>.hjson` in cwd, then
  exits cleanly.  New `inkhaven recover
  <crash.hjson>` CLI lifts unsaved buffers back
  on restart.

Goal: zero panics from the hot path, zero data
loss across crashes, every project-wide
inconsistency either auto-fixed or surfaced.
Goal is asymptotic — the cycle's concrete exit
criteria are spelled out in §9 of the proposal.

See `Documentation/PROPOSALS/1.2.15_PLAN.md`
for the full phase shape, test targets (target:
637 → ≥ 700, +63 new regression tests), and risk
register.

Phase G (Git / backup safety rails — uncommitted-
changes warning, backup-freshness chip, disk-
space gate) is out of scope for 1.2.15 and
deferred to 1.2.16.

## Planned themes

### Carryovers from 1.2.14

#### Snippet expansion — picker placeholders + bund: prefix

The Q.2 snippet system ships with 8 built-in
placeholders that resolve synchronously inside
`expand_placeholders`.  Three placeholders + one
prefix were deferred because they need to pause
the synchronous expansion pipeline for an async
event:

  * **`{char_lookup}` / `{place_lookup}` /
    `{artefact_lookup}`** — open the corresponding
    system-book picker and substitute the chosen
    entry's display name (or wiki-link target,
    depending on the surrounding text).  Needs a
    pending-snippet state machine: the editor
    stashes the partial expansion, opens the picker
    modal, and resumes the substitution on commit.
  * **`bund:` prefix** — when the snippet body
    starts with `bund:`, the rest is interpreted
    as a Bund-VM program whose output (top of stack
    as string) becomes the expansion.  Needs a Bund
    interpreter handle on the snippet expansion
    path plus error handling for non-stringable
    results.  Pairs naturally with the existing
    `ink.*` stdlib (`ink.fs.read_file`,
    `ink.editor.*`).

  Both unlock advanced authoring flows but require
  the same underlying mechanism — once the async
  state machine lands, all four arrive together.

#### Language-book D.2 carryovers

Carried from 1.2.13 into 1.2.14 and still queued.
Mostly CLI exports + headless workflow tools:

  * **`inkhaven language export --format grammar`**
    — printable grammar reference (TOC + chapter
    per rule category + examples tables + cross-
    refs).  Schema is already there; the exporter
    needs to parse + format.
  * **`inkhaven language export --format phrasebook`**
    — Sample-text chapter rendered as a phrasebook
    (gloss on left, working language on right).
  * **`inkhaven language export --format csv`** —
    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.  Pairs with CI gates.
  * **`inkhaven language translate <language>
    <text>`** — headless flavour of the in-TUI
    `Ctrl+B Q` chord.  Shell-pipe-friendly.
  * **`inkhaven language define-rule <language>
    <rule_id> --category <cat>`** — opens the rule
    template in `$EDITOR` for hand-editing the
    HJSON.  Pairs with the deferred `--format
    grammar` exporter.

#### Language-book UX polish (from 1.2.13)

  * **`Ctrl+B Shift+R` reverse-lookup picker** in
    Language book — find dictionary entry by
    translation.  `LanguageEntryIndex` already
    keys by surface form; 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 inside
    the Language book.
  * **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
    `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 → 1.2.14;
    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, 1.2.13 tags were never pushed at all;
    1.2.14 has both a tag AND a pushed remote, but
    no GitHub Release UI object.  Backfill the
    missing tags (cherry-pick from each release
    commit) and create Release objects for all
    four, then either keep refreshing the gh CLI
    `workflow` scope before each release or
    automate via `.github/workflows/release.yml`.

## Implementation log

### Theme R — Crash report writer + recovery (R.1 + R.2)

**R.1** — new `src/crash` module: `CrashContext`
singleton + `install_panic_hook()` chained to the
default Rust hook + atomic-write helper + global
terminal-restore slot the TUI sets/clears around
its raw-mode session.

* `CrashReport` struct (version, panic context,
  project context, rescued buffers, recent action
  ring of last 50 user actions, environment
  fingerprint, process stats).  HJSON
  serialization; panic message truncated at 4 KB.
* `actions::ActionRecord` + `ActionRing` —
  capped chronological ring with structural
  detail strings.
* `rescue::DirtyMirror` + `flush_dirty_buffers` —
  writes every dirty buffer to
  `<original>.inkhaven-rescue` atomically; per-
  file errors recorded as `RescueOutcome` and
  don't abort the rest of the flush.

Wiring: `main.rs` installs the hook before any
code that might panic; TUI registers a terminal-
restore closure on raw-mode enter; `App`
maintains the dirty-buffer mirror via a 2 s
debounce in the main loop; `save_current` clears
mirrors on dirty → clean transitions; `run_action`
pushes an `action.<Variant>` record on every
dispatched action.

14 new tests covering atomic-write, action-ring
rotation, HJSON roundtrip, rescue flush, error
handling.

**R.2** — new `inkhaven recover <crash.hjson>`
CLI.  Walks the rescued-buffer manifest; per-buffer
prompt accepts `y` / `N` / `diff`.  On apply:
copies the current on-disk file to
`<original>.pre-recover-<UTC>` then atomically
replaces it with the rescue bytes via
`io_atomic::write`.  `--yes` skips prompts;
`--keep` retains files in cwd after apply
(default moves them to
`<project>/.inkhaven/recovered/`).

4 new tests covering: empty-report flow,
identical-content short-circuit, full apply path
with backup verification, `--keep` retention.

### Theme S — Stability sweep (S.1 — S.5)

Five sub-phases, 62 panic sites + 2 latent
out-of-bounds crashes + 5 non-atomic critical-
data writes + 4 lock-poisoning sites neutralised.

**S.1** — sweep `src/tui/app/**`.  43
`unreachable!()` arms in the `draw_modal` match
collapsed into a single graceful catch-all that
logs and returns instead of panicking on an
unhandled Modal variant.  5 `expect("opened
checked above")` in render/panes.rs converted to
`let-else return` graceful no-ops.  2 ai_impl.rs
panic sites: `unreachable!()` replaced with
`debug_assert! + log + return`; `lang_books.first().
unwrap()` replaced with `if let [only] = ...`
pattern.

**S.2** — sweep `src/tui/app/render/**`.  Two
real edge-case crashes patched: empty HJSON editor
buffer (`highlighted.len().max(1)` clamp would
push past actual length and index out-of-bounds);
stale fuzzy paragraph picker indices (after a
tree mutation invalidated `entries`).  Rest of
the slice indexing in `render/modals.rs` audited
and confirmed bounded-by-construction.

**S.3** — sweep `src/store/**` + `src/cli/**`.
Only 2 production sites: `serde_json::to_string_
pretty(...).unwrap()` in thread doctor JSON
output replaced with `?` via `Error::Store`;
`hierarchy.get(parent_id).expect(...)` in
`reindex` orphan-import loop replaced with
`let-else + warn + continue`.

**S.4** — sweep helper modules + atomic-write
upgrade.  Zero panic sites in
`src/tui/snippets.rs` / `comments.rs` /
`lexicon.rs` / `project_goal.rs` etc.  Instead
surfaced a more important stability gap: every
user-data save path used non-atomic `std::fs::
write`.  New `src/io_atomic.rs` module exposes
the temp + fsync + rename + parent-dir fsync
primitive; converted save paths:
`editor_impl.rs:save_current` (the manuscript
save path), `save_doc` (secondary editor),
`comments.rs:save_to_sidecar`, `session.rs:save`,
`app.rs:import_directory paragraph create`.

**S.5** — lock-poisoning recovery + invariant
tightening.  4 `cache.lock().unwrap()` sites in
`typst_world.rs` (font + per-World source caches)
converted to `unwrap_or_else(|p| p.into_inner())`
so a prior panic in a critical section doesn't
crash every subsequent typst compile.  6 panic
sites in `assemble.rs` / `language_entry.rs` /
`prompts_tui/app.rs` / `scrivener/import.rs` /
`storage/vector.rs` / `timeline/calendar.rs`
converted to type-level invariants (`if let
Some(...)`, slice patterns, `let-else +
continue`).

### Theme H — Background health monitor (H.1 — H.3)

**H.1** — new `src/health` module.  `HealthEvent`
enum (Ok / Warning / Repaired / Error),
`HealthFinding` (class, severity, detail,
auto_repairable), `HealthClass` enum (Db / Index
/ Vectors / Editor / Tree / Disk / Backup /
Project), `Severity`, `ChipState` glyphs (✓ ✎
⚠ ✗).  `spawn_monitor()` returns `Option<
UnboundedReceiver<HealthEvent>>`.  TUI wiring:
`App.health_rx` attached after `App::new`,
`tick_health_pump()` drains events on every
main-loop iteration, `health_chip_spans()`
renders the status-bar chip between focus + POV.
H.1 ships one stub check (project root reachable)
to verify the channel works end-to-end.

**H.2** — three real disk-side checks landed:

* Project root reachable (90 s cadence).
* Backup freshness vs. `backup.max_age`
  (5 min cadence).
* Rescue file orphans older than 7 days
  (1 h cadence).

Plus `collapse_findings()` — per-tick worst-case
collapse so the chip reflects the most severe
state instead of flashing through intermediate
Ok frames.  6 new tests.

**H.3** — auto-repair flow + log file.  New
`src/health/log.rs` writes pipe-separated event
log to `.inkhaven/health.log` with 1 MB ×
5-archive size rotation.  Per-class
`RepairPolicy` opt-in.  First repair landed:
rescue-orphan sweep (deletes files older than
30 d when `health.auto_repair.rescue_orphans`
is true).  6 new tests covering log rotation +
repair flow.

### Theme D — Doctor scan + autofix (D.1 — D.3)

**D.1** — new `src/cli/doctor_scan.rs`.  Four
scan classes (zero-byte-file, orphan-paragraph-
row, missing-referenced-file, corrupt-comments-
sidecar).  `--scan` / `--json` / `--class
<name>` flags on `inkhaven doctor`.  Exit code 2
when any finding at Warning+ shipped.  5 new
tests.

**D.2** — `--autofix` flow + audit log.
`apply_fix(project, &finding)` per-class repair
function.  `--autofix` prompts y/N per finding;
`--yes` skips prompts.  Every repair logged to
`<project>/.inkhaven/doctor.log` with UTC | OK/
ERR | class | detail.  Smoke-tested end-to-end:
zero-byte file → delete DB row + file → log
entry recorded.

**D.3** — TUI doctor panel.  New
`Action::OpenDoctorPanel` (rebindable as
`view.doctor_panel`); chord `Ctrl+B Shift+0`
(paired with `Ctrl+B 0` config-editor in the
digit-0 "system inspection" cluster).  New
`Modal::DoctorPanel { findings, cursor, scroll,
last_status }`.  Modal-local chords: ↑↓
navigate, `r` repair highlighted (removes from
list on success), `R` repair all, `Esc` close.

**Follow-up after the proposed phases:** investigation
of a real-project run revealed 28 "orphan-paragraph-
row" findings were actually false-positive
warnings — every paragraph had its content
preserved in bdslib (the editor's `load_paragraph`
falls back to `store.get_content` precisely for
this case).  New `ScanClass::BdslibOnly` (Info
severity) for the recoverable case; orphan
warnings now only fire when both disk AND bdslib
are empty.  `apply_fix(BdslibOnly)` rematerializes
the disk file from bdslib content via
`io_atomic::write` (non-destructive — refuses to
clobber a non-empty disk file).

### Phase S.6 — Security hardening

Post-proposal addition driven by a security
audit.  Audit catalogued 4 risk classes; all
patched in 1.2.15.

New `src/path_safety.rs` — `resolve_within(root,
candidate)` rejects absolute paths and `..`
traversal; `resolve_within_or_absolute` permits
absolute paths for config fields with documented
intent.  9 tests.

**H3 — recover CLI path traversal.**  Crash-
report `paragraph_rel_path` validated via
`resolve_within_str` before joining onto the
project root.  Crafted reports with
`paragraph_rel_path: "../../etc/passwd"` rejected
with a security error.

**H4 — HJSON config path traversal.**
`prompts_file` (in `src/project.rs`) and
`artefacts_directory` (in `src/store/mod.rs`)
filtered for `..` escapes in relative paths.
Absolute paths still honoured per documented
intent.  Rejections fall back to safe defaults
with a warning log.

**H2 — `ink.fs.*` sandbox.**  `ink.fs.read` /
`ink.fs.write` now confine paths to the project
root by default via `resolve_within_str`.  Writes
upgraded to atomic via `crate::io_atomic::write`.
New `scripting.fs_unsandboxed: bool` policy
opt-out for trusted-use cases (default false).

**H1 — Script auto-load trust gate.**  New
`scripting.trust_decision` HJSON field: `"ask"`
(default) requires `<project>/.inkhaven/trust`
with a `trust` marker line; `"trust"` runs
unconditionally; `"deny"` never runs.  File-based
gate (not interactive prompt) so it works in
both TUI and CLI flows.  7 tests covering
explicit/file/case/unknown-value paths.

Audit findings not yet addressed (deferred):

* **M1** — recent-action ring in crash report may
  capture pasted secrets in `detail` field.
  Mitigation: review report before sharing
  (documented in `SECURITY_WARNING.md` §3.5).
* **M3** — script-initiated `ink.input` modals
  could mimic legitimate prompts.  Mitigation:
  trust gate (H1) is the primary defence.

### Documentation — SECURITY_WARNING

New `Documentation/SECURITY_WARNING.md` — full
risk disclosure: license + no-warranty, limitation
of liability (no personal / business / financial
damages), user responsibilities (back up, audit
untrusted projects, review crash reports before
sharing), known issues (H1-H4 fixed, M1/M3
pending, 4 inherent design properties), unknown
risks (undiscovered vulns, dependency vulns,
evolving Bund surface, platform differences,
doc/code drift), no indemnification, reporting
channel, acceptance via continued use.  README
"Licence" section ends with a Security Warning
pointer.

## Closing

1.2.15 shipped the full proposed scope (R.1, R.2,
S.1-S.5, H.1-H.3, D.1-D.3) plus an unplanned
security hardening phase (S.6.H1-H4) that emerged
from the audit, plus documentation for the new
risk disclosure model.

Tests went 565 → 695 (+130).  Zero new
dependencies.  No phase missed; nothing deferred
to 1.2.16 from the original proposal except the
language-book carryovers (D.2 export formats, UX
polish) and the snippet picker placeholders —
both held over because they need new internal
mechanisms (async snippet state, fs2/sysinfo
disk-free measurement) that are scope of their
own follow-ups.

The residual hot-path panic surface in inkhaven
is now tiny — and anything that does panic
produces a survivable crash report via R.1 + R.2.
