A 3-Way Text Merger That Never Shows Conflict Markers

Conflict-free 3-way text merging for Rust, JavaScript, and Python. Both sides' edits survive, cursors move with them, and no conflict markers appear.

The reconcile-text logo and tagline "Conflict-free 3-way text merging".

reconcile-text merges two conflicting edits of the same text into one, without asking a human to sort it out. Where a traditional 3-way merge stops and writes <<<<<<< markers, it applies both sets of changes using an algorithm inspired by Operational Transformation, and repositions any cursors and selections along the way. It’s a Rust library with WebAssembly bindings for JavaScript and native bindings for Python; the interactive demo1 shows it merging as you type.

The whole API is one call. Give it the common ancestor and the two versions that drifted from it:

import { reconcile } from 'reconcile-text';

// the version both sides start from
const parent = 'Hello world';

// one user added "beautiful"
const left = 'Hello beautiful world';

// the other changed the greeting
const right = 'Hi world';

// "Hi beautiful world"
reconcile(parent, left, right).text;

All three packages expose the same function with the same semantics: cargo add reconcile-text, npm install reconcile-text, or pip install reconcile-text, depending on where you need it.

Why

Merging concurrent edits is a solved problem, provided you control the whole editing stack. CRDTs and Operational Transformation both work by capturing every individual operation as it happens, which is fine when you own the editor, the transport layer, and the storage format. Many workflows aren’t like that: an Obsidian vault gets edited by everything from Vim to VS Code, and all a sync engine ever sees is the final state of each file. That’s a Differential Synchronisation scenario: the last synced parent plus two divergent children, with no record of the keystrokes that produced them.

It’s the same problem Git addresses, except Git hands the hard cases back to you as conflict markers. That’s the right call for source code, where an incorrect merge is a bug and a human has to verify the result. reconcile-text bets that human text is more forgiving: a slightly imperfect sentence is usually better than conflict markers interrupting the flow of a document. (Not every kind of text qualifies: in a legal contract, two edits that combine into a double negation quietly change the meaning. And for code the problem runs the other way: a merge can be semantically wrong even with no syntactic conflict.)

So the library does exactly one thing: three strings in, one string out, every time. It’s the merge primitive underneath VaultLink, my Obsidian sync engine; differential sync only feels right if the merge step never needs a human.

How it works

It starts off like diff3, then adds the conflict-resolution step diff3 refuses to take. Given the parent and the two modified versions:

  1. Tokenisation. The texts are split into the units the merge will operate on: words by default, or characters, lines, or a custom tokeniser. This is a bigger lever than it looks: at word granularity, most prose “conflicts” dissolve into adjacent edits that can both survive.
  2. Diff computation. Myers’ algorithm produces two edit scripts, parent → left and parent → right.
  3. Diff optimisation. The operations are reordered and consolidated so related changes chain together.
  4. The weave. The two scripts are combined using OT principles: each side’s edits are transformed over the other’s, so every modification lands, and cursor positions are carried through each transformation.

I never set out to implement OT for its own sake; transforming batched diffs just happens to be an elegant way to merge two Myers outputs. The same could be achieved with a CRDT. But when all you can observe is end states, merge quality is capped by the quality of the underlying 2-way diffs, whichever machinery does the merging. A moved paragraph, for instance, reaches the merger as an unrelated delete and insert, because that’s all Myers’ algorithm can say about it.

The whole pipeline handles Unicode properly: full UTF-8, with grapheme clusters kept intact so complex scripts never get split mid-character.

Where it sits among the alternatives

Every nearby tool stops short of this job in a different place.

diff3 and Git

diff3 and git merge-file do the structural work, then write <<<<<<< markers whenever both sides touch the same region; so do the libraries that reimplement them: diffy and merge3 in Rust, node-diff3 in JavaScript. reconcile-text shares their diff3-like foundation; the difference is the resolution step that eliminates markers entirely.

diff-match-patch

diff-match-patch is Neil Fraser’s widely used library from his time at Google: character-level Myers diffing, fuzzy matching, and patch application, powering his Differential Synchronisation protocol. It’s the closest tool in spirit, and it differs in four ways that matter here:

  • 2-way, not 3-way. It diffs two texts and applies the result as a patch to a third. There’s no concept of a common ancestor, so it can’t reason about what the left and right sides each intended.
  • Character-level only. Word- or line-level diffing requires encoding tokens as single Unicode characters first; reconcile-text tokenises natively.
  • Patches can fail. patch_apply reports per-patch success, and failed patches are dropped. Inside a sync loop, that failure self-corrects on the next cycle, but in a one-shot merge, the edit is simply lost. reconcile-text always produces a complete merged result.
  • No cursor tracking or provenance. It won’t reposition cursors or tell you which side made which edit; reconcile-text does both.

The repo carries a runnable comparison2 with concrete inputs where diff-match-patch garbles adjacent edits and silently drops an entire sentence; reconcile-text merges both correctly. When you genuinely have no common ancestor (two texts that diverged through an unknown sequence of edits), diff-match-patch is the right tool; with an ancestor, the 3-way merge wins.

CRDTs

Yjs, Automerge, Loro, cola, and diamond-types guarantee convergence by construction: every operation commutes, so application order stops mattering. They capture each operation with a unique identity, work peer-to-peer, scale past two concurrent editors, and never lose an edit. The trade-off is state: an operation log or internal structure that grows with the document’s history. You can’t hand a CRDT library three plain strings and ask for a merge; that’s exactly the gap reconcile-text fills. The advice cuts both ways: if you do control the whole editing stack, a CRDT gives you stronger guarantees, and it handles N editors natively where reconcile-text merges exactly two forks at a time (though merges can be chained).

Operational Transformation

OT libraries like ot.js and ShareJS transform live operations against each other, typically with a central server deciding the canonical order. reconcile-text borrows the transformation concept but aims it at a different problem: instead of individual keystrokes in real time, it transforms the consolidated diffs of two complete edits. No server, no operation capture, fully offline. If you need sub-second real-time collaboration and can run a coordination server, use the real thing; this library is for merge points, not keystroke-by-keystroke sync.

One core, three registries

The Rust core compiles to WebAssembly through wasm-bindgen for the npm package and binds natively to Python through pyo3, so all three languages run the same merge logic. The strangest target is React Native: Hermes, its default engine, exposes no WebAssembly global at runtime, so the package’s react-native entry point ships a pure-JavaScript build of the same core, transpiled from the WASM by Binaryen’s wasm2js. Slower, but it behaves the same anywhere JavaScript runs.

If you’d like to poke at the merge behaviour without installing anything, the demo1 runs that same WASM build directly in your browser.

  1. interactive demo: https://schmelczer.dev/reconcile/
  2. runnable comparison: https://git.schmelczer.dev/andras/reconcile