inkhaven 1.6.21

Inkhaven — TUI literary work editor for Typst books
# 1.3.32 — Project doctor: referential integrity + the test spine

The first 1.4.0 bookend (see [ROADMAP-1.4.0.md](ROADMAP-1.4.0.md)). Two deliverables:
a **data-integrity safety net** and the **test/bench harness** every later release
will lean on.

## What already exists

`inkhaven doctor --scan` (`src/cli/doctor_scan.rs`) is a mature scanner with 15
classes across two families:

- **Project-integrity** (disk-side): `ZeroByteFile`, `OrphanParagraphRow`,
  `MissingReferencedFile`, `CorruptCommentsSidecar`, `BdslibOnly`, `StaleSubmission`.
- **Editorial** (narrative): dropped characters, pacing, stalled threads, naming,
  echoes, numeric contradictions, continuity drift, unresolved tension, long
  paragraphs.

Architecture: `ScanClass` enum (`slug`/`from_slug`/`ALL`/`is_opt_in`/
`editorial_category`), `ScanSeverity` (Critical/Warning/Info), `ScanFinding {class,
severity, path, detail}`, `ScanReport`, and `scan_project(project, selected)` that
gates each `scan_*(...)` behind a `run(class)` guard. `--json`, `--class <slug>`,
`--autofix` already wired in `cli/mod.rs`; exit code 2 when any finding is
≥ Warning.

## The gap

Every existing project-integrity class is **disk-side** (file vs DB). None checks
**referential integrity** *within* the hierarchy — the dangling UUIDs that
accumulate from deletes, partial restores, and hand-edits. That's the genuine fsck
hole, and it's cheap: the whole node set is already in memory after
`Hierarchy::load`.

## Phases

- **P0 — referential-integrity scan classes.** Five new `ScanClass` variants, all
  pure over the loaded `Hierarchy` (no disk, no DB mutation), wired into
  `scan_project` and the default run. **(this increment)**
  - `BrokenParentRef` *(Critical)*`parent_id` points at a node that doesn't
    exist → the node is detached/unreachable.
  - `DanglingParagraphLink` *(Warning)* — a `linked_paragraphs` UUID resolves to
    nothing (a deleted target).
  - `DanglingEventRef` *(Warning)* — an `event.characters` / `event.places` UUID
    resolves to nothing.
  - `SiblingSlugCollision` *(Warning)* — two children of the same parent share a
    slug → their on-disk `fs_path`s collide.
  - `DuplicateSystemBook` *(Warning)* — more than one Book carries the same
    `system_tag`.
- **P1 — the test/bench spine.** _Done._ Decision: crate dev-deps are fine (only
  external *application/binary* deps are off-limits). **criterion was already wired**
  (`benches/` + `[[bench]]` startup/search), so P1 only added **`proptest`**. Perf
  budgets land as in-process `#[test]`s with an `Instant` assertion (the right tool
  for CI regression gates — runs in the release-gate `cargo test`; criterion stays
  for manual profiling). Shipped: a referential-scan perf-budget test (1000 nodes
  < 500ms, guards against O(n²) regressions) + a proptest harness smoke (3
  `levenshtein`-is-a-metric properties). Full suite 1758 → 1762.
- **P2 — parser property tests.** _Done._ Property sweep over the three named
  panic-prone parsers on the P1 spine: **calendar** (`parse` never-panics across all
  presets; `format` never-panics on a wide tick range × every precision; `sols`
  day-precision exact round-trip; `format` canonical-through-`parse` for all
  presets), **HJSON config** (`serde_hjson::from_str::<Config>` never-panics on
  arbitrary input *and* brace/quote soup), and **language detection** (`detect` /
  `detect_with_confidence` never-panic + deterministic + survive empty / emoji /
  100k-char inputs). 9 property tests; full suite 1762 → 1771.
- **P3 — autofix for the safe classes.** Prune dangling `linked_paragraphs` /
  `event` refs and reconcile under `doctor --autofix` (store mutation; the existing
  autofix path). Detection-only in P0.

## Non-goals

No change to the existing scan classes, the report schema, the `--json` / `--class`
/ `--autofix` surface, or the editorial scanner. No new runtime dependencies.

## Increment log

- **P0**_done._ Five referential-integrity `ScanClass` variants added to
  `doctor_scan.rs` (`BrokenParentRef` Critical; `DanglingParagraphLink`,
  `DanglingEventRef`, `SiblingSlugCollision`, `DuplicateSystemBook` Warning), each a
  pure `scan_*(&Hierarchy)` pass wired into `scan_project` + the default run, with
  `slug`/`from_slug`/`ALL` updated and a detection-only `apply_fix` arm. 6 unit
  tests (via `Hierarchy::from_nodes_for_test`) + live smoke. Full suite 1752 → 1758.
</content>