divot 0.1.0

Fast diff/patch engine, in Rust, with a Node.js binding (Python planned)
Documentation
# divot

Fast diff/patch engine, in Rust, with Python and Node bindings.

## Why

[jsdiff](https://github.com/kpdecker/jsdiff) (138M weekly downloads) is
the dominant diff library on npm, and its own docs admit a real
performance compromise: linked-list diagonal tracking instead of
optimized array-based Myers diff. divot's core is built on
[`imara-diff`](https://github.com/pascalkuthe/imara-diff) — the diff
engine that powers `gitoxide` in production — which doesn't make that
trade-off, and additionally implements the Histogram algorithm (git's
modern default: faster *and* more human-readable for code than plain
Myers).

## Benefits

- **~4x faster than jsdiff for a single diff**, ~11x for batch workloads
  (many files at once) — both measured through the real compiled native
  addon, not the underlying Rust library in isolation. See
  [Performance]#performance for the full, honest breakdown, including
  where the numbers came *down* from bigger ones once FFI cost was
  accounted for.
- **`jsdiff`-shaped API** (`diffLines`/`diffWords`/`diffChars`, the same
  `{value, added, removed, count}` result shape) — switching from `diff`
  is close to a drop-in change, not a rewrite.
- **Batch diffing with built-in parallelism** (`diffLinesMany`) — a
  capability jsdiff structurally can't offer (it's single-threaded with
  no batch API at all). Useful for linting a whole PR or diffing every
  file in a directory in one call.
- **Real correctness guarantees, not just speed**: every diff function
  is tested to reconstruct the original `before`/`after` text exactly
  from its output, including multibyte UTF-8 — not just "looks right on
  an example."
- Built on `imara-diff`, the same diff engine trusted in production by
  `gitoxide` — not a from-scratch reimplementation of something
  security/correctness-sensitive.

## Installation

**Not yet published to npm or PyPI.** To use it today, build from
source:

```sh
git clone https://github.com/hsputra/divot.git
cd divot
npm install
npm run build   # compiles the native addon via napi-rs
```

This produces `index.js`/`index.d.ts` (already committed) plus a
platform-specific `divot.<platform>.node` binary. `require("./index.js")`
(or `require("/path/to/divot")`, which resolves the same way via
`package.json`'s `main` field) from there.

## How to use

```js
// Once published: require("divot"). Until then, per Installation above,
// require the path to your local build instead:
const { diffLines, diffWords, diffChars, unifiedDiff, diffLinesMany } = require("./index.js");
```

### `diffLines(before, after)`

```js
const before = "function greet(name) {\n  console.log('Hello ' + name);\n}\n";
const after  = "function greet(name) {\n  console.log(`Hello ${name}!`);\n}\n";

for (const part of diffLines(before, after)) {
  const prefix = part.added ? "+" : part.removed ? "-" : " ";
  console.log(prefix + part.value.trimEnd());
}
//  function greet(name) {
// -  console.log('Hello ' + name);
// +  console.log(`Hello ${name}!`);
//  }
```

### `diffWords(before, after)` / `diffChars(before, after)`

Same shape, finer granularity:

```js
diffWords("the quick brown fox", "the slow brown fox");
// [
//   { value: "the ", added: false, removed: false, count: 2 },
//   { value: "quick", added: false, removed: true, count: 1 },
//   { value: "slow", added: true, removed: false, count: 1 },
//   { value: " brown fox", added: false, removed: false, count: 4 },
// ]
```

### `unifiedDiff(before, after)`

Real `git diff`/`diff -u`-style patch text:

```js
console.log(unifiedDiff(before, after));
// @@ -1,3 +1,3 @@
//  function greet(name) {
// -  console.log('Hello ' + name);
// +  console.log(`Hello ${name}!`);
//  }
```

### `diffLinesMany(pairs)` — batch, parallel across CPU cores

For diffing many files at once (the realistic shape of CI/lint/AI-agent
workloads), not a loop of individual `diffLines` calls:

```js
const results = diffLinesMany([
  { before: fileAContentOld, after: fileAContentNew },
  { before: fileBContentOld, after: fileBContentNew },
  // ...
]);
// results[i] is exactly what diffLines(pairs[i].before, pairs[i].after)
// would return -- diffLinesMany just does all of them across a Rayon
// thread pool instead of one at a time.
```

Only reaches for this when you actually have many pairs — for a single
diff, plain `diffLines` is the right call; see
[Performance](#performance) for why.

## Performance

Real, measured, reproducible — not projected. 270 before/after pairs
extracted from jQuery's actual git history (`src/event.js`, `css.js`,
`core.js`, `manipulation.js`, `selector.js`, `ajax.js`, `effects.js`,
`deferred.js`), diffed with the real `diff` npm package (jsdiff v9.0.0)
and with divot, same corpus, both warmed up first.

| | Time/pair | vs. jsdiff |
|---|---|---|
| jsdiff v9.0.0 (`diffLines`) | 118.96µs | — |
| **divot, via the real npm binding** | **29.9µs** | **~4.0x** |
| divot, pure Rust (no FFI) | ~19.8µs | ~5.9x |

**~4.0x is the number that matters** — it's what the compiled npm
binding actually delivers (not yet published, but built and measured
exactly as it will ship — see [Installation](#installation)), measured
through the real compiled native addon, not the underlying Rust library
in isolation. The pure-Rust figure is shown for transparency about where
FFI (UTF-16→UTF-8 conversion in, JS object construction out) costs real
time, not to imply it's what you'll observe from JS.

<img src="plots/jsdiff_comparison.svg" alt="Per-diff latency: divot vs jsdiff, log-log scatter across 270 real pairs" width="600">
<img src="plots/jsdiff_speedup.svg" alt="Per-diff speedup: divot vs jsdiff across 270 real pairs, mean marked" width="600">

Full methodology, raw CSV data, and the chart-generation script are in
[`plots/`](plots/).

### Batch diffing: a real capability jsdiff can't offer

`diffLinesMany()` diffs many pairs at once across all available CPU
cores (via Rayon internally) — not an algorithm improvement over
`imara-diff`, a different capability entirely: jsdiff is single-threaded
with no built-in batch API, so this isn't something it can match by
being faster, only by the caller manually parallelizing across worker
threads/processes themselves.

| | Time/pair | vs. jsdiff (looped) |
|---|---|---|
| jsdiff v9.0.0, called in a loop | ~118.4µs | — |
| **`diffLinesMany`, real npm binding, 18 cores** | **~10.5µs** | **~11x** |

Verified this is genuinely about parallelism and not some batching
trick: forcing a single Rayon thread reproduces the non-batched ~19.8µs
pure-Rust number exactly. **The multiplier scales with available cores**
— it's a batch-throughput number, not a claim that any single diff got
11x faster. Useful for realistic bulk workloads (lint a whole PR, diff
every file in a directory), less useful for a one-off single diff, where
`diffLines()` is the right call.

<img src="plots/batch_scaling.svg" alt="Batch/parallel diffing throughput vs thread count, divot vs jsdiff single-threaded reference line" width="600">

## Status

Early. Implemented and tested: line/word/char diffing, unified diff
output, npm binding (`diffLines`/`diffWords`/`diffChars`/`unifiedDiff`/
`diffLinesMany`, real native addon, real tests via Node's built-in test
runner). No CLI yet.

Not yet implemented: Python bindings (PyO3), a fuzzy/atomic
patch-application layer (the planned differentiator for LLM-coding-agent
workflows — see the project's technical spec for detail on the failure
modes it targets). Nothing here is published to any registry yet.

## Credit

Built on [`imara-diff`](https://github.com/pascalkuthe/imara-diff)
(Apache-2.0) by Pascal Kuthe — the diff computation itself is that
crate's work; this project adds word/char tokenization (not provided
upstream), a `jsdiff`-compatible result shape, batch/parallel diffing,
and (planned) the patch-application layer.

## License

Dual-licensed under MIT or Apache-2.0, at your option.