A diff engine that tells you what actually happened to your code.
git diff compares lines. The line-diff approach dates back to the original Unix diff in 1974, and it still has no idea what those lines mean. Rename a function and you get a deletion plus an addition. Move a block of code across files and you get two unrelated chunks of noise. Reformat a file and you get "everything changed." It works, but it makes you do the thinking.
Differens parses your code into trees, matches nodes between them, and tells you what changed in terms you actually use: renamed, moved, extracted, added, removed, reformatted only.
- Parse both sides into a structured tree using tree-sitter
- Match nodes between trees with a top-down/bottom-up algorithm (GumTree lineage)
- Emit a typed edit script: Insert, Delete, Update, Move
- Narrate the edit script into readable output
The core is deterministic. Same inputs produce the same output every time. No model runs anywhere in the pipeline.
| What changed | What you get |
|---|---|
| Function renamed | renamed function parse_config to load_config |
| Code moved across files | moved function validate from utils.ts to validators.ts |
| Class added | added class RetryPolicy |
| Config key changed | changed database.pool.max from 10 to 25 |
| Whitespace only | reformatted only, no logical changes |
And when it can't parse something, it falls back gracefully. Unparseable code falls back to structural tree diff. That falls back to line diff. That falls back to "changed / unchanged." The tool never refuses to give you an answer.
npm install -g differensOr run it without installing:
npx differensNode 18.17 or newer. The tree-sitter grammars ship as prebuilt binaries for the common platforms, so there is nothing to compile.
The same build is published under the ossl scope as
@ossl-dev/differens-cli.
Identical package, identical differens command; install whichever name you
prefer, not both.
The engine is published in pieces, so you can take the matching core without the tree-sitter grammars, or the narration without git.
| Package | What it gives you |
|---|---|
@ossl-dev/differens-core |
diffTrees, the node model, typed edit scripts. No dependencies. |
@ossl-dev/differens-tiers |
Turns source, config and markup into trees the core can match. Brings the grammars. |
@ossl-dev/differens-narrate |
Edit script to sentences, markdown, JSON, or the compact model format. |
@ossl-dev/differens-git |
Working tree, commit range and directory diffs; the diff driver. |
@ossl-dev/differens-correlate |
Finds code that moved between files. |
import { diffTrees, treeFromValue } from "@ossl-dev/differens-core";
const before = treeFromValue({ retries: 3, host: "a.example" });
const after = treeFromValue({ retries: 5, host: "a.example" });
diffTrees(before, after).changes;
// [{ type: "Update", node: { kind: "leaf", label: "retries", ... },
// detail: { kind: "ValueChanged", from: "3", to: "5" } }]Diffing files rather than values means going through the tier router, which picks a parser from the path:
import { diffWithTier } from "@ossl-dev/differens-tiers";
import { formatChanges, narrate } from "@ossl-dev/differens-narrate";
const { changes } = diffWithTier(oldSource, newSource, "src/app.ts", "src/app.ts");
console.log(formatChanges(narrate(changes), { format: "llm" }));ESM only, types included.
From source, or as a standalone executable
bun install
bun run apps/cli/src/index.ts <inputs>
# single-file executable
bun build apps/cli/src/index.ts --compile --outfile differensThe grammars are native addons and cannot be embedded in a --compiled
executable, so a standalone binary line-diffs source files unless it is run
from a directory where the grammars are installed. Use the npm install for
semantic diffing.
Differens is a diff tool, so the CLI is the diff. No subcommand needed.
differens # diff working tree vs HEAD
differens a.ts b.ts # diff two files
differens old/ new/ # diff two directories
differens main..feature # diff a commit range
differens 2a8178e 3a5015f # diff two commits by id or branch name
differens a.json b.json --format=llmdiff is kept as an explicit alias (differens diff a.ts b.ts).
| Flag | Use |
|---|---|
| (default) | Terminal, one line per change with scope: changed value of port from 3000 to 8080 in object root |
--format=json |
Raw SemanticChange array, for tooling |
--format=markdown |
Rolled-up summary, for PR descriptions |
--format=llm |
Dense line format for AI tools: one line per change, with source line numbers. Roughly 15x smaller than the git diff it replaces |
LLM format is line-oriented, one file heading then one line per change. Unnamed churn (comments, prose lines, bare expressions) collapses into a count, and every named change carries its source line, so a model can read the twenty lines around a change instead of the whole file:
differens/1 3 files 380 changes 113 named
# apps/cli/src/index.ts
+ function runWorker :373
- function mapWithConcurrency :313
~ function report :141 handleGitDiff -> report
* 12 comments, 3 expressions
# config/app.json
~ leaf port :14 < object database 3000 -> 8080
# cross-file
> validate utils.ts -> validators.ts
Ops are + added, - removed, ~ changed, > moved, * rolled-up count.
:N is the source line and < Kind name is the enclosing scope.
On this repo's own 14-file changeset that format is 6.5KB against 100KB of
git diff.
differens languages # what's supported: semantic vs generic per language
differens install-git-driver # register as a git difftool
differens --help # usage
differens --version # version numberdifferens/
├── packages/
│ ├── core/ # tree representation, matching algorithm, edit scripts
│ ├── tiers/ # format adapters: markup, data, code, prose, composite
│ ├── correlate/ # cross-file move and rename detection
│ ├── narrate/ # template engine: edit script -> English, output formats
│ ├── git/ # git integration: diff driver, ranges, directory walk
│ └── tsconfig/ # shared TypeScript config
└── apps/
└── cli/ # the differens command line tool
- Deterministic core. Same inputs, same output, every time. CI-safe by design.
- Graceful degradation. Every tier falls back to the one below it. No hard failures.
- Git-aware, not git-dependent. Everything works standalone; git is a convenience layer on top.
- Fast enough for every commit. Target: under 100ms overhead per typical file.
Milestone 0 shipped: GumTree-lineage matching core (53-bit Merkle hashing, postorder
index, Dice bottom-up, LIS-minimised moves), JSON/YAML/TOML adapters, tree-sitter code
adapter with TypeScript/Python/Rust/Go extractors, git integration (working tree, commit
ranges, commit pairs, batched blob reads), directory diffing, a cross-file correlator, and
the narration engine with terminal/markdown/json/llm output. Per-file diffs run on a
process pool. ~100 tests, zero failures. Published on npm as
differens, runs on Node.
- difftastic -- tree-sitter structural diff in Rust. Closest existing tool.
- GumTree -- the original top-down/bottom-up AST matching algorithm (Falleri et al., 2014)
- mergiraf -- tree-sitter AST merging, the natural next problem after diffing
MIT