# Inkhaven 1.3.10 — Semantic drift

_On top of 1.3.8's world-consistency tools. 1.3.8 caught **hard**
contradictions — a fact clashing with a fact. 1.3.10 catches **soft** drift:
two descriptions of the same entity that diverge across the manuscript without
a clean factual clash — a tavern "cramped and smoky" in ch.2, "airy and
bright" in ch.20. It's also the bridge to the 1.4 Whole-Book AI Editor: it
builds and proves the retrieve-then-reason loop. Full plan:
[WORLD-2_PLAN.md](../PROPOSALS/WORLD-2_PLAN.md)._

## The honest design: embeddings retrieve, the AI adjudicates

Neither half works alone. Cosine similarity finds same-topic descriptions but
can't tell contradiction from relatedness ("cramped and smoky" and "airy and
bright" are highly *similar* yet opposite). An antonym lexicon is hopelessly
brittle. So drift splits the work: **embeddings retrieve** the handful of
paragraphs that describe an entity, and an **AI pass adjudicates** which
contradict.

Crucially, it **reuses the vector index that's already built as you write** —
every paragraph is embedded on save (the same index `Ctrl+F` searches). No new
indexing step, no bulk re-embed.

## `inkhaven drift scan`

For each Character, Place, and Artefact, Inkhaven retrieves the paragraphs that
describe it (keeping only the ones that actually *name* it — the anchor that
drops topical false-positives), then asks the model which descriptions
contradict:

```
drift scan: 1 description contradiction(s):
  ⚠ The Drunken Goose (place) — [ch.2] “cramped and smoky”  ⟷  [ch.20] “airy and bright”
      ↳ the same tavern can't be both without a renovation the story never shows
```

Cached in `.inkhaven/drift.json` (order-independent content hash), `--json` for
a CI gate. `inkhaven drift list` previews the retrieved descriptions
deterministically, no AI.

## In the Editorial Pass

Drift contradictions surface in `inkhaven edit` and the `Ctrl+V Shift+R`
cockpit as the `drift` category (Warn), ranked with everything else and
**jump-only** — `Enter` lands on the *later*, divergent passage. There's no
honest single-paragraph rewrite for "the atmosphere changed across 18
chapters"; you reconcile the two passages. `inkhaven edit --deep` now runs the
drift scan alongside the facts / tension / continuity ones.

## In the story bible

`Ctrl+V Shift+L` becomes a continuity-at-a-glance view: under any flagged
entity, a **⚠ drift** badge (amber) names the contradicting descriptions, the
model's reason, and the entity's full chapter-ordered **description trail** —
every passage that described it, in order, each a jump to its source. The scan
persists the trail in the sidecar, so the bible renders it with no embedding
load in the TUI.

## Tuning

```hjson
drift: {
  top_k: 24          // vector hits pulled per entity before name-filtering
  max_snippets: 8    // descriptions kept per entity (bounds the judge prompt)
}
```

## Under the hood

- Zero new dependencies (fastembed + genai were already in the tree). The
  retrieval reuses the on-save vector index; the judge mirrors the
  `facts check` AI-pass + content-hashed sidecar pattern.
- New pure, unit-tested code in `src/drift.rs`: `assemble_descriptions` (name
  anchor + dedup + chapter order + cap), `parse_drift_pairs`,
  `resolve_conflicts`, the `DriftReport` round-trip.
- Honest limits, stated in the tutorial: retrieval-bounded (misses
  pronoun-only description), AI-judged (needs a provider), needs the index
  populated.
- Tests 1339 → 1346. New tutorial 70; quick-help + the `Ctrl+V Shift+L`
  description refreshed.

## Carryovers (still open)

- **The Whole-Book AI Editor** (general retrieve-then-reason over any query) —
  the 1.4 headline this cut is the bridge to.
- **1.2.20:** ePub inline images + popup footnotes.
- PDF follow-ups: N-up / booklet quick-impose presets; CMYK-JPEG grayscale.
- A TUI `edit --deep` trigger; sixth supported language; GitHub Release
  backfill for 1.2.11–1.2.19.
