# v1.2.1 — bdslib absorbed · Bund scripting · data-driven keymap

Largest single release since 1.1. Three intertwined themes:

1. **Crates.io readiness** — the last two vendored path-deps
   (`bdslib`, `tree-sitter-typst`) are now absorbed into the
   inkhaven source tree. `cargo publish` works.
2. **Bund scripting** — Vladimir's `bundcore` stack VM is wired
   in as a first-class extension surface. Hook points, an
   `ink.*` stdlib, sandbox policy, `.bund` Script nodes, runtime
   keymap rebinding.
3. **Data-driven keymap** — `Ctrl+B` and `Ctrl+Z` sub-chords are
   no longer hardcoded match arms; they live in a table that
   HJSON and Bund can both rewrite.

## Headline changes

### Storage layer absorbed in-tree

The bdslib dependency that owned `metadata.db`, `blobs.db`, and
`vectors/` is gone. The slice inkhaven actually used (~11
methods of `DocumentStorage`) lives at `src/storage/`:

- `engine.rs` — DuckDB pool + JsonStorage + BlobStorage
- `vector.rs` — vecstore HNSW wrapper with a `dirty` flag so
  background syncs no-op when nothing's changed
- `embedding.rs` — fastembed model wrapper
- `document.rs` — the 11-method API surface
- `fingerprint.rs` — `json_fingerprint`

On-disk shape is **byte-compatible** with 1.0–1.1 projects.
Existing `frequency.db` is harmlessly ignored.

- **998 → 635 crates** in `Cargo.lock` (Tantivy, augurs,
  bundcore-shard, etc. — none of it inkhaven ever touched).
- **66 MB → 63 MB** release binary.

### Tree-sitter-typst absorbed in-tree

Same pattern, smaller scope. The grammar's C sources live at
`src/grammar/`; `build.rs` compiles them via `cc`. With this and
bdslib absorbed, **no path-deps remain** and the crate is
`cargo publish`-ready.

### Bund scripting

Bund is now embedded as inkhaven's user-customisation surface.
See [`Documentation/Bund/`](../Bund/README.md) for the tutorial.

- **Adam VM** — process-wide singleton, lazily initialised on the
  first eval, holds state across calls.
- **`inkhaven bund "<code>"`** CLI subcommand for one-shot
  evaluation.
- **Six read-only `ink.*` stdlib words**:
  - `ink.node.list`, `ink.node.get`, `ink.node.children`
  - `ink.paragraph.text`
  - `ink.search.text`
  - `ink.snapshot.list`
- **Sandbox policy** in `scripting:` HJSON stanza —
  `disabled_categories` / `disabled_words` / `enabled_words` /
  `enabled_categories` / `no_default_deny`. Defaults deny
  `fs_write` / `net` / `shell` / `code_eval` / `keymap`.
- **Bootstrap script** — `scripting.bootstrap: '''…'''` runs
  once after the VM is built.
- **Five hook points** in `Store::*`:
  - `hook.on_create  ( uuid kind -- )`
  - `hook.on_save    ( uuid -- )`
  - `hook.on_rename  ( uuid new_title -- )`
  - `hook.on_snapshot ( parent_uuid snap_uuid -- )`
  - `hook.on_delete  ( uuid -- )` (per id in deleted subtree)
- **Recursion guard** caps hook depth at 4 (thread-local
  counter, forward-looking for write-side ink.* words).

### `NodeKind::Script` + Scripts system book

Bund scripts are first-class tree nodes:

- **`.bund` files** on disk under `books/<...>/`
- **Tree-pane glyph**: `λ` (mauve, Catppuccin)
- **Auto-loaded** at project open — every Script node's body is
  `bund.eval`'d into Adam after the inline bootstrap, so user
  hook lambdas come online without any HJSON wiring
- **Scripts** is the 8th system book (between Typst and Help),
  the default home for project-global hooks
- **Editor open / save** routes through the same `load_paragraph`
  / `update_paragraph_content` path as `.typ` nodes
- **CLI**: `inkhaven add script "title" --parent scripts`
- **TUI**: `Ctrl+Z N` opens Add modal pre-pointed at Scripts

### Bund chord prefix `Ctrl+Z`

Parallel to `Ctrl+B`. Three default sub-chords:

| Chord | Action |
|---|---|
| `Ctrl+Z R` | Run the open `.bund` buffer against Adam |
| `Ctrl+Z N` | New script under Scripts book |
| `Ctrl+Z E` | One-shot eval modal — type expression, see result |

`Ctrl+Z` is configurable via `keys.bund_prefix` (set to empty
to disable for tmux users). tui-textarea's default undo (which
also lived on Ctrl+Z) is on **Ctrl+U** in this codebase, so the
prefix doesn't fight a meaningful builtin.

### Data-driven keymap with HJSON + Bund rebinding

The ~40 chord-action mappings under `Ctrl+B` and `Ctrl+Z` moved
from hardcoded `match key.code` arms into a runtime-mutable
binding table. Two channels rewrite it:

**HJSON overlay** — `keys.bindings`:

```hjson
keys: {
  bindings: [
    { chord: "Ctrl+b y", action: "tree.morph_type" }
    { chord: "Ctrl+b w", action: "none" }                  // disable
    { chord: "Ctrl+z m", action: "bund.run_buffer" }
  ]
}
```

**Bund runtime** — four `ink.key.*` words (default-denied via
the `keymap` policy category):

```bund
"Ctrl+b y" "tree.morph_type" ink.key.bind
"Ctrl+b j" { "jot!" println } ink.key.bind_lambda
"Ctrl+b y" ink.key.unbind
ink.key.list
```

See [`Documentation/KEYS_REASSIGNMENT.md`](../KEYS_REASSIGNMENT.md)
for the full action table and both channels.

### New chord: `Ctrl+B M` — morph type

Cycle a leaf node's flavour:

```
Paragraph(typst) → Paragraph(hjson) → Script(bund) → back
```

Renames the file on disk (`.typ` ↔ `.hjson` ↔ `.bund`), stamps
new kind/content_type, reopens the buffer with the right
highlighter. Works from both Tree and Editor panes; preserves
the user's focus across the reopen.

### Per-kind tree glyphs

- `¶` Paragraph (typst)
- `❴` Paragraph (hjson)
- `λ` Script (bund)
- `▣` Image
- `►` currently-open in editor

Replaces the previous "all leaves render as `¶`" behaviour
where HJSON paragraphs and Scripts rendered as branch-arrow
glyphs (a real bug — Scripts fell through the `_ =>` case).

### Bund syntax highlighter

Hand-rolled tokeniser at `src/tui/bund_highlight.rs`. Wired
into the editor for any node with `content_type="bund"`:

- `//` line comments → dim grey
- `"…"` strings → green (cross-line state for unterminated)
- `42` / `3.14` / `-7` numbers → peach
- `{` / `}` lambda braces → mauve (matches tree row colour)
- `register` / `println` / `if` / `drop` / …  ~40 keywords → cyan
- `ink.*` / `hook.*` namespace → function colour

### Auto-generated status-bar hints

Pressing `Ctrl+B` or `Ctrl+Z` now displays a hint built from the
live binding table. User overlays (HJSON or `ink.key.*`) appear
in the hint automatically — no more out-of-sync hardcoded strings.

### Background sync — dirty flag + DuckDB CHECKPOINT

- `VectorEngine` now tracks a `dirty: AtomicBool`. Background
  sync (`sync_interval_seconds`, **default 600s** up from 60s)
  is a no-op when no writes have happened since the last flush.
- New `DocumentStorage::checkpoint()` runs DuckDB `CHECKPOINT;`
  against `metadata.db` + `blobs.db`. Wired into the same
  background tick and into TUI shutdown via
  `App::shutdown_flush`.
- Net result: idle editor produces zero disk traffic; long
  sessions don't accumulate WAL.

## New configuration

```hjson
keys: {
  bund_prefix:        Ctrl+z          // new
  bindings:           []              // new — see KEYS_REASSIGNMENT.md
}

scripting: {                          // entirely new stanza
  disabled_categories: []
  disabled_words:      []
  enabled_words:       []
  enabled_categories:  []
  no_default_deny:     false
  bootstrap:           ""
}

sync_interval_seconds: 600            // was 60
```

## New CLI subcommands

- `inkhaven bund "<code>"` — evaluate a Bund expression against
  Adam, print result. Optionally `--project <path>` to register
  the store so `ink.*` words can see it.
- `inkhaven add script "title" --parent scripts` — create a
  `.bund` Script node from the shell.

## New documentation

- [`Documentation/Bund/BUND_TUTORIAL.md`](../Bund/BUND_TUTORIAL.md) — afternoon-length intro to Bund
- [`Documentation/Bund/README.md`](../Bund/README.md) — Bund overview + index
- [`Documentation/KEYS_REASSIGNMENT.md`](../KEYS_REASSIGNMENT.md) — full action table, HJSON + Bund rebinding channels
- [`Documentation/RELEASE_NOTES/1.2.1.md`](1.2.1.md) — this document

## Breaking changes

- **`Ctrl+Z` is no longer free.** It's the Bund-meta prefix now.
  tui-textarea's default undo binding is shadowed; undo has
  always been on `Ctrl+U` in this codebase, so this only affects
  external chord muscle memory. Set `keys.bund_prefix: ""` in
  HJSON to disable and recover `Ctrl+Z` for your terminal
  multiplexer.
- **`sync_interval_seconds` default went 60s → 600s.** Projects
  with an explicit value are unchanged; defaulting projects see a
  10× longer interval. The interval is now a no-op when the index
  is clean, so the bigger number costs nothing.
- **bdslib path-dep removed from `Cargo.toml`.** Anyone who
  pinned `bdslib` for their own tooling needs to redirect to
  github.com/vulogov/bdslib directly.
- **`frequency.db`** in existing projects is now read-only
  (inkhaven no longer writes to it). Leave it on disk or delete —
  inkhaven is indifferent.

## Migration checklist

1. **Upgrade**: `cargo install inkhaven` or `cargo binstall inkhaven` (binaries attached to this release).
2. **Try a Bund expression**: `inkhaven bund "40 2 +"` should print `42`.
3. **Inside a project**: `inkhaven --project <path> bund 'ink.node.list'` — should JSON-dump every node.
4. **Open the TUI**: `Ctrl+B M` should cycle the cursor row's leaf type. `Ctrl+B` shows the new auto-generated hint string.
5. **Read** [`Documentation/Bund/BUND_TUTORIAL.md`](../Bund/BUND_TUTORIAL.md) if you want to write hooks or rebind chords.

## Release stats

The bdslib + tree-sitter-typst absorptions were ~2k LoC of net
deletions each; the Bund integration is ~1.5k LoC of net new
code.

Suite: 144 / 144 tests, no failures.
