# Road to 2.0 — Performance & Stability Verification
2.0 is gated on this document passing. 2.0 itself is the **semantic-net / knowledge-graph
flagship** — see the RFC at `Documentation/PROPOSALS/SEMNET-1_PLAN.md`; the 1.x runway
converges the feature surface, fixes performance, and hardens — and *this* is the harness
+ checklist that proves it. (SEMNET-P8 lands a `graph` bench + graph crash/rebuild +
multilingual round-trip into the audits below, and closes this gate.)
The measurement harness is built first so every subsequent change is measured, not
guessed.
## The measurement harness
Benches are **subprocess-based** (inkhaven is a bin crate; a lib+bin split — the enabler
for in-process render microbenching — is a tracked future refactor). They run the release
binary against a generated fixture.
### Fixtures
`inkhaven gen-fixture <path>` builds a deterministic project. Knobs:
```
inkhaven gen-fixture /tmp/fix \
--books 5 --chapters 20 --paragraphs 100 \ # 10 000 ¶ (default)
--target-words 450 --seed 0xC0FFEEDEADBEEF \
--languages en,ru,fr,de,es \ # multilingual (round-robin per book)
--force
```
- **Scale:** `--books 25` → 50 000 ¶; shrink for quick runs. Same seed + shape → byte-identical.
- **Multilingual (2.0 harness):** `--languages en,ru,fr,de,es` spreads Cyrillic / accented /
umlaut / ñ prose across the corpus, exercising tokenisation → embedding → search →
export end to end. (Book titles carry a `[lang]` tag.)
### Running the benches
```
cargo build --release
./target/release/inkhaven gen-fixture /tmp/inkhaven-bench --force
INKHAVEN_BENCH_FIXTURE=/tmp/inkhaven-bench cargo bench
```
Benches:
- `startup` (cold/warm list), `search` (3 query classes).
- **`render`** — editor frame-render cost. Drives the hidden `_bench-render`, which draws
N frames against a headless `TestBackend` and reports the *internal* time (startup
excluded), so the figure is the true per-frame draw cost — a regression guard on the
editor's render path.
- **`scale`** — list/search across corpus sizes. Point it at several fixtures:
`INKHAVEN_BENCH_FIXTURES="1k=/p1:10k=/p2:50k=/p3" cargo bench --bench scale` (label=path,
colon-separated); falls back to the single `INKHAVEN_BENCH_FIXTURE` labelled `default`.
- **`export`** — `export markdown/typst/epub` on a dedicated single-book fixture the
bench generates at setup (so no `--book-name` is needed).
- **`index`** — embedding throughput (the index-build cost), via the hidden `_bench-embed`
which times the engine on N multilingual texts (model-load excluded as a warm-up).
- **`graph`** — SEMNET knowledge-graph edge-store throughput, via the hidden `_bench-graph`:
bulk edge-insert (`graph/insert`) + reverse-index neighbour-query latency
(`graph/neighbor_query`). Self-contained (temp store, no fixture / network) — gated. The
structural regression it guards: losing the reverse index (→ O(n) neighbour scans) or an
O(n²) insert.
### Reference measurements (indicative — this machine, small fixtures)
Not the gating baseline — CI's `target/criterion` artifact from a `main` run on a fixed
fixture is authoritative (see `bench.yml`). These orient the eye and prove the harness:
| `render/editor_frame` | ~183 µs / frame | 60 ¶ |
| `index/embed` | ~4.9 ms / embed (≈205/s) | n/a (engine only) |
| `export/markdown` | ~39 ms | 100 ¶, 1 book |
| `export/typst` | ~32 ms | 100 ¶, 1 book |
The full `search`/`startup`/`scale` baseline is generated by the CI bench gate on the
standard 10k-¶ fixture. Regenerate locally with `gen-fixture` + `cargo bench`.
### Idle CPU & frame render
These are TUI-internal and *not* subprocess-benchable. Until the lib split lands, measure
**idle CPU** empirically: launch the editor on a large fixture, leave it untouched 60 s,
sample CPU (`top`/`ps`). The main loop repaints every ~200 ms even when idle (≈5 fps).
**Event-driven redraw is postponed indefinitely (decision 2026-07-31).** Repainting only
on a dirty flag would zero out idle CPU, but the ≈5 fps idle cost is negligible in
practice (a terminal app at rest), and the change would thread a dirty-flag through every
input/stream/timer path — real complexity and regression surface for a win nobody feels.
Not worth it. The `render` bench stays as a per-frame regression guard; idle CPU is
accepted as-is rather than gated.
## Performance targets (proposed baselines to commit)
| Warm startup → first frame | < 300 ms | `startup` bench |
| **Idle CPU** | accepted as-is (~5 fps) | manual sample — *not gated* (event-driven redraw postponed) |
| Frame render p50 / p99 | < 8 / 16 ms (large book) | `render` bench (regression guard) |
| Search p50 / p99 @ 10k ¶ | < 50 / 200 ms | `search` bench |
| Save + reembed / ¶ | < 100 ms | `save` bench |
| Embedding / index build | linear in ¶ | `index` bench |
| **Graph edge insert / query** | linear insert · O(log n) neighbour query | `graph` bench |
| RSS @ 10k / 50k ¶ | sub-linear, documented | `/usr/bin/time -l` |
| Regression gate | no > 10% vs baseline | bench compare (`_bench-report`) |
## Stability checklist (the 1.2.15 bar, re-asserted for 2.0)
Status as verified for the **2.0.0 cut (2026-08-01)**. `[x]` = covered by the test suite
/ a run pass; `[~]` = covered by design + tests but no fresh dedicated pass this cut;
`[ ]` = not run for this cut (noted honestly).
- [x] **No reachable panics** — a fresh **full-codebase 12-area multi-agent panic audit**
(2026-08-01, ~289K LOC, refute-by-default adversarial verify) found **0 confirmed
reachable panics**. (The earlier 6-finder audit, 2026-07-31, had surfaced 4, all fixed
with regression tests, `c9278deb`.) The SEMNET layer is panic-free in non-test paths.
- [x] **Every persistent write is atomic** — `io_atomic` for sidecars; DuckDB
transactions for `metadata`/`blobs`/**`edges.db`** (batch insert commits-or-rolls-back,
tested); no swallowed persist `Result`s; poison recovery on the pool.
- [~] **Fuzz / property** — decompression-bomb caps (DEM + EPUB `read_capped`, `c9278deb`);
config / `world.hjson` / import parsers degrade to errors, not panics. No fresh
full-surface fuzz this cut.
- [~] **Multilingual round-trips** — the multilingual `gen-fixture` + the graph's
en/ru/fr/de/es parity (contradiction / lexical-ILI / locus canonicalization) are
tested; the export/import paths retain their prior lossless coverage.
- [x] **Concurrency & soak** — a dedicated soak (`inkhaven _soak`, 2026-08-01) ran
**171,399 iterations over 600 s** (~286/s) of sustained node CRUD + edge
insert/query/cascade-GC + periodic embedding (exercising the bg vector-sync) +
checkpoints, with an integrity check every 10 s heartbeat. Result: **integrity clean
throughout** (59/59 heartbeats `ok`), **RSS bounded + stable** (peaked ~1.28 GB on
model-load, settled to a ~311–415 MB steady-state band — *no* leak), `live`/`edges`
counts bounded. The soak also surfaced + fixed a latent bug: `integrity_check` ran a
SQLite-only `PRAGMA` DuckDB lacks (now a DuckDB-native full-scan probe). (A 10-min
soak, not hours — a longer run remains available via `_soak --seconds N`.)
- [x] **Crash / restore** — `edges.db` survives checkpoint + reopen and `kill -9` mid-write
by DuckDB WAL + atomic commit (tested: `edge_survives_checkpoint_and_reopen`,
`insert_batch_commits_and_rolls_back`); the meta/blob stores keep their WAL/atomic
guarantees; backup → restore unchanged.
- [x] **Data integrity** — provenance / verdict / locus round-trips tested; the graph is
provably rebuildable (`graph rebuild` re-derives the Structural subset); no orphaned
nodes or edges after delete/move (cascade GC tested).
- [x] **Graph integrity** (SEMNET) — `edges.db` reopens consistent after checkpoint/crash;
`graph rebuild` deterministically reconstructs `Structural` while preserving
`Authorial`/`Promoted`/`Judged`/`Imported` (the P4 contract, verified end-to-end with
the lexical bridge surviving a rebuild); multilingual contradiction / lexical parity
across en/ru/fr/de/es; **no orphaned edges** after node delete/move; `graph`-store
`integrity_check` PRAGMA available.
### Release verification (2.0.0, 2026-08-01)
Run for the cut: **full test suite green (2769 pass, 0 failed, warning-free)**; clean
`--release` build; the **`graph` bench green** (`graph/insert` ~574 µs, `graph/neighbor_query`
~376 µs) + the render/scale/export gate benches; the SEMNET panic self-audit.
**Post-cut completion (2026-08-01, on 2.0.1-dev):** the two items deferred at the cut are
now done — a **fresh full-codebase 12-area multi-agent panic audit (0 confirmed reachable
panics)** and a **171k-iteration / 10-min soak (integrity clean, RSS bounded, no leak)**.
The soak fixed a latent `integrity_check` bug (SQLite `PRAGMA` → DuckDB full-scan). Every
gate box is now genuinely checked.
## The 2.0 flagship — SEMNET
2.0 *is* the semantic net. The RFC (`PROPOSALS/SEMNET-1_PLAN.md`) + implementation plan
(`SEMNET-1_IMPL.md`) are the design; the entire arc **SEMNET-P0…P9 has shipped**
(substrate · structural lift · provenance/verdicts · stance persistence · bibliographic ·
lexical bridge · surfacing · inner-family · bench-gate · capstone docs) plus the follow-ups
(snowball→`Cites`, declared-world→`Declares`, and the in-editor `Ctrl+V g` + confront
promote/dismiss). The graph is a first-class, crash-safe, benched, multilingual, documented
typed-edge layer over the nodes — surfaced across both the CLI and the editor.
## The 2.0 gate — CUT as 2.0.0 (2026-08-01)
The gate: perf targets met (`graph` bench green, no regressions) · panics audited + fixed ·
every write atomic (incl. `edges.db`) · multilingual parity · graph integrity clean · docs
true. Verified for the cut per the checklist + Release-verification note above; the one
box left open (a dedicated long soak) is recommended ongoing diligence, not a blocker the
owner chose to wait on. **Shipped as 2.0.0.**
## Known platform note — Windows (DEFERRED)
**Supported release targets: Linux (`x86_64-unknown-linux-gnu`) and macOS
(`aarch64-apple-darwin`).** Windows is **deferred** — soft-failing in CI, no release asset.
**Root cause (confirmed 2026-07-31):** the `x86_64-pc-windows-gnu` build fails at `ort-sys`
— the ONNX Runtime bindings `fastembed` pulls in for embeddings ship prebuilt binaries for
windows-**msvc** but **not** windows-**gnu**. Upstream (pyke/ort), not our code; the MSYS2
toolchain (duckdb C++, fontconfig) itself builds fine.
**Decision:** push a Windows release to the future, unblocked by any of —
1. **ort ships a windows-gnu prebuilt** → the existing MSYS2 job just goes green (kept
ready in `release.yml`); or
2. **adopt the windows-msvc target** — ort *has* msvc prebuilts, so provenance is fixed at
build time, the **same security model as Linux/macOS** (the recommended path; needs the
codebase made msvc-clean); or
3. **bundle a build-time-verified `onnxruntime.dll`** in the release zip, loaded by absolute
path.
**Rejected: downloading + loading `onnxruntime.dll` at runtime/startup.** A DLL is
executable native code run in-process with no sandbox — unlike the fastembed *model* (data).
Runtime fetch-and-load adds DLL-hijack (Windows search-order), supply-chain/MITM, and
tamper-at-rest surface; it is strictly more attack surface than options 1–3, and stretches
the project's "no external-binary deps" principle. Not worth it for a non-gate platform.
Resolving Windows is independent of the 2.0 gate above.