Repository files navigation

Differens

CInpm versionLicense: MIT

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.

📖 Documentation: full API reference, guides, and architecture deep-dive.

How it works (the short version)

  1. Parse both sides into a structured tree using tree-sitter
  2. Match nodes between trees with a top-down/bottom-up algorithm (GumTree lineage)
  3. Emit a typed edit script: Insert, Delete, Update, Move
  4. 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 it handles

What changedWhat you get
Function renamedrenamed function parse_config to load_config
Code moved across filesmoved function validate from utils.ts to validators.ts
Class addedadded class RetryPolicy
Config key changedchanged database.pool.max from 10 to 25
Whitespace onlyreformatted 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.

Install

npm install -g differens

Or run it without installing:

npx differens

Node 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.

Use it as a library

The engine is published in pieces, so you can take the matching core without the tree-sitter grammars, or the narration without git.

PackageWhat it gives you
@ossl-dev/differens-corediffTrees, the node model, typed edit scripts. No dependencies.
@ossl-dev/differens-tiersTurns source, config and markup into trees the core can match. Brings the grammars.
@ossl-dev/differens-narrateEdit script to sentences, markdown, JSON, or the compact model format.
@ossl-dev/differens-gitWorking tree, commit range and directory diffs; the diff driver.
@ossl-dev/differens-correlateFinds code that moved between files.
import{diffTrees,treeFromValue}from"@ossl-dev/differens-core";constbefore=treeFromValue({retries: 3,host: "a.example"});constafter=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 differens

The 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.

Usage

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=llm

diff is kept as an explicit alias (differens diff a.ts b.ts).

Output formats

FlagUse
(default)Terminal, one line per change with scope: changed value of port from 3000 to 8080 in object root
--format=jsonRaw SemanticChange array, for tooling
--format=markdownRolled-up summary, for PR descriptions
--format=llmDense line format for AI tools: one line per change, with source line numbers. Roughly 15x smaller than the git diff it replaces
--format=ndjsonOne JSON object per changed file, streamed in input order as results land

differens.toml or .differensrc.json in the repo root sets the default format and the git driver extension list; flags override it.

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.

Other commands

differens languages # what's supported: semantic vs generic per language
differens install-git-driver # register as a git diff driver, writes .gitattributes
differens --help # usage
differens --version # version number

Project structure

differens/
├── 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

Design principles

  • 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.

Status

Milestone 0 shipped: GumTree-lineage matching core (53-bit Merkle hashing, postorder index, Dice bottom-up, LIS-minimised moves, full content verification behind every hash match), JSON/YAML/TOML/INI/env adapters, tree-sitter code adapter with extractors for sixteen languages (TypeScript/JavaScript, Python, Rust, Go, C, C++, Java, Ruby, PHP, Swift, Kotlin, C#, Scala, Lua, shell), git integration (working tree, commit ranges, commit pairs, batched blob reads, self-writing .gitattributes), directory diffing with cross-directory rename detection, a cross-file correlator, streaming ndjson output, and the narration engine with terminal/markdown/json/llm output. Per-file diffs run on a process pool, and a content-addressed parse cache reuses trees within a run. 420 tests, zero failures. Published on npm as differens, runs on Node.

Prior art worth reading before contributing

  • 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

License

MIT

About

A diff engine that tells you what actually happened to your code, moved, renamed, extracted, reformatted, instead of which lines changed. Algorithm-first, AI-optional, works with or without git.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all \u003cpre\u003e\u003ccode\u003e blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks"); } } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); } })(); (function(){ try { var __m = "github.com"; var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

Differens

CInpm versionLicense: MIT

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.

📖 Documentation: full API reference, guides, and architecture deep-dive.

How it works (the short version)

  1. Parse both sides into a structured tree using tree-sitter
  2. Match nodes between trees with a top-down/bottom-up algorithm (GumTree lineage)
  3. Emit a typed edit script: Insert, Delete, Update, Move
  4. 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 it handles

What changedWhat you get
Function renamedrenamed function parse_config to load_config
Code moved across filesmoved function validate from utils.ts to validators.ts
Class addedadded class RetryPolicy
Config key changedchanged database.pool.max from 10 to 25
Whitespace onlyreformatted 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.

Install

npm install -g differens

Or run it without installing:

npx differens

Node 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.

Use it as a library

The engine is published in pieces, so you can take the matching core without the tree-sitter grammars, or the narration without git.

PackageWhat it gives you
@ossl-dev/differens-corediffTrees, the node model, typed edit scripts. No dependencies.
@ossl-dev/differens-tiersTurns source, config and markup into trees the core can match. Brings the grammars.
@ossl-dev/differens-narrateEdit script to sentences, markdown, JSON, or the compact model format.
@ossl-dev/differens-gitWorking tree, commit range and directory diffs; the diff driver.
@ossl-dev/differens-correlateFinds code that moved between files.
import{diffTrees,treeFromValue}from"@ossl-dev/differens-core";constbefore=treeFromValue({retries: 3,host: "a.example"});constafter=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 differens

The 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.

Usage

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=llm

diff is kept as an explicit alias (differens diff a.ts b.ts).

Output formats

FlagUse
(default)Terminal, one line per change with scope: changed value of port from 3000 to 8080 in object root
--format=jsonRaw SemanticChange array, for tooling
--format=markdownRolled-up summary, for PR descriptions
--format=llmDense line format for AI tools: one line per change, with source line numbers. Roughly 15x smaller than the git diff it replaces
--format=ndjsonOne JSON object per changed file, streamed in input order as results land

differens.toml or .differensrc.json in the repo root sets the default format and the git driver extension list; flags override it.

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.

Other commands

differens languages # what's supported: semantic vs generic per language
differens install-git-driver # register as a git diff driver, writes .gitattributes
differens --help # usage
differens --version # version number

Project structure

differens/
├── 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

Design principles

  • 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.

Status

Milestone 0 shipped: GumTree-lineage matching core (53-bit Merkle hashing, postorder index, Dice bottom-up, LIS-minimised moves, full content verification behind every hash match), JSON/YAML/TOML/INI/env adapters, tree-sitter code adapter with extractors for sixteen languages (TypeScript/JavaScript, Python, Rust, Go, C, C++, Java, Ruby, PHP, Swift, Kotlin, C#, Scala, Lua, shell), git integration (working tree, commit ranges, commit pairs, batched blob reads, self-writing .gitattributes), directory diffing with cross-directory rename detection, a cross-file correlator, streaming ndjson output, and the narration engine with terminal/markdown/json/llm output. Per-file diffs run on a process pool, and a content-addressed parse cache reuses trees within a run. 420 tests, zero failures. Published on npm as differens, runs on Node.

Prior art worth reading before contributing

  • 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

License

MIT

About

A diff engine that tells you what actually happened to your code, moved, renamed, extracted, reformatted, instead of which lines changed. Algorithm-first, AI-optional, works with or without git.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Differens

CInpm versionLicense: MIT

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.

📖 Documentation: full API reference, guides, and architecture deep-dive.

How it works (the short version)

  1. Parse both sides into a structured tree using tree-sitter
  2. Match nodes between trees with a top-down/bottom-up algorithm (GumTree lineage)
  3. Emit a typed edit script: Insert, Delete, Update, Move
  4. 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 it handles

What changedWhat you get
Function renamedrenamed function parse_config to load_config
Code moved across filesmoved function validate from utils.ts to validators.ts
Class addedadded class RetryPolicy
Config key changedchanged database.pool.max from 10 to 25
Whitespace onlyreformatted 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.

Install

npm install -g differens

Or run it without installing:

npx differens

Node 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.

Use it as a library

The engine is published in pieces, so you can take the matching core without the tree-sitter grammars, or the narration without git.

PackageWhat it gives you
@ossl-dev/differens-corediffTrees, the node model, typed edit scripts. No dependencies.
@ossl-dev/differens-tiersTurns source, config and markup into trees the core can match. Brings the grammars.
@ossl-dev/differens-narrateEdit script to sentences, markdown, JSON, or the compact model format.
@ossl-dev/differens-gitWorking tree, commit range and directory diffs; the diff driver.
@ossl-dev/differens-correlateFinds code that moved between files.
import{diffTrees,treeFromValue}from"@ossl-dev/differens-core";constbefore=treeFromValue({retries: 3,host: "a.example"});constafter=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 differens

The 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.

Usage

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=llm

diff is kept as an explicit alias (differens diff a.ts b.ts).

Output formats

FlagUse
(default)Terminal, one line per change with scope: changed value of port from 3000 to 8080 in object root
--format=jsonRaw SemanticChange array, for tooling
--format=markdownRolled-up summary, for PR descriptions
--format=llmDense line format for AI tools: one line per change, with source line numbers. Roughly 15x smaller than the git diff it replaces
--format=ndjsonOne JSON object per changed file, streamed in input order as results land

differens.toml or .differensrc.json in the repo root sets the default format and the git driver extension list; flags override it.

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.

Other commands

differens languages # what's supported: semantic vs generic per language
differens install-git-driver # register as a git diff driver, writes .gitattributes
differens --help # usage
differens --version # version number

Project structure

differens/
├── 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

Design principles

  • 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.

Status

Milestone 0 shipped: GumTree-lineage matching core (53-bit Merkle hashing, postorder index, Dice bottom-up, LIS-minimised moves, full content verification behind every hash match), JSON/YAML/TOML/INI/env adapters, tree-sitter code adapter with extractors for sixteen languages (TypeScript/JavaScript, Python, Rust, Go, C, C++, Java, Ruby, PHP, Swift, Kotlin, C#, Scala, Lua, shell), git integration (working tree, commit ranges, commit pairs, batched blob reads, self-writing .gitattributes), directory diffing with cross-directory rename detection, a cross-file correlator, streaming ndjson output, and the narration engine with terminal/markdown/json/llm output. Per-file diffs run on a process pool, and a content-addressed parse cache reuses trees within a run. 420 tests, zero failures. Published on npm as differens, runs on Node.

Prior art worth reading before contributing

  • 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

License

MIT

About

A diff engine that tells you what actually happened to your code, moved, renamed, extracted, reformatted, instead of which lines changed. Algorithm-first, AI-optional, works with or without git.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length \u003e 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Differens

CInpm versionLicense: MIT

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.

📖 Documentation: full API reference, guides, and architecture deep-dive.

How it works (the short version)

  1. Parse both sides into a structured tree using tree-sitter
  2. Match nodes between trees with a top-down/bottom-up algorithm (GumTree lineage)
  3. Emit a typed edit script: Insert, Delete, Update, Move
  4. 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 it handles

What changedWhat you get
Function renamedrenamed function parse_config to load_config
Code moved across filesmoved function validate from utils.ts to validators.ts
Class addedadded class RetryPolicy
Config key changedchanged database.pool.max from 10 to 25
Whitespace onlyreformatted 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.

Install

npm install -g differens

Or run it without installing:

npx differens

Node 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.

Use it as a library

The engine is published in pieces, so you can take the matching core without the tree-sitter grammars, or the narration without git.

PackageWhat it gives you
@ossl-dev/differens-corediffTrees, the node model, typed edit scripts. No dependencies.
@ossl-dev/differens-tiersTurns source, config and markup into trees the core can match. Brings the grammars.
@ossl-dev/differens-narrateEdit script to sentences, markdown, JSON, or the compact model format.
@ossl-dev/differens-gitWorking tree, commit range and directory diffs; the diff driver.
@ossl-dev/differens-correlateFinds code that moved between files.
import{diffTrees,treeFromValue}from"@ossl-dev/differens-core";constbefore=treeFromValue({retries: 3,host: "a.example"});constafter=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 differens

The 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.

Usage

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=llm

diff is kept as an explicit alias (differens diff a.ts b.ts).

Output formats

FlagUse
(default)Terminal, one line per change with scope: changed value of port from 3000 to 8080 in object root
--format=jsonRaw SemanticChange array, for tooling
--format=markdownRolled-up summary, for PR descriptions
--format=llmDense line format for AI tools: one line per change, with source line numbers. Roughly 15x smaller than the git diff it replaces
--format=ndjsonOne JSON object per changed file, streamed in input order as results land

differens.toml or .differensrc.json in the repo root sets the default format and the git driver extension list; flags override it.

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.

Other commands

differens languages # what's supported: semantic vs generic per language
differens install-git-driver # register as a git diff driver, writes .gitattributes
differens --help # usage
differens --version # version number

Project structure

differens/
├── 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

Design principles

  • 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.

Status

Milestone 0 shipped: GumTree-lineage matching core (53-bit Merkle hashing, postorder index, Dice bottom-up, LIS-minimised moves, full content verification behind every hash match), JSON/YAML/TOML/INI/env adapters, tree-sitter code adapter with extractors for sixteen languages (TypeScript/JavaScript, Python, Rust, Go, C, C++, Java, Ruby, PHP, Swift, Kotlin, C#, Scala, Lua, shell), git integration (working tree, commit ranges, commit pairs, batched blob reads, self-writing .gitattributes), directory diffing with cross-directory rename detection, a cross-file correlator, streaming ndjson output, and the narration engine with terminal/markdown/json/llm output. Per-file diffs run on a process pool, and a content-addressed parse cache reuses trees within a run. 420 tests, zero failures. Published on npm as differens, runs on Node.

Prior art worth reading before contributing

  • 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

License

MIT

About

A diff engine that tells you what actually happened to your code, moved, renamed, extracted, reformatted, instead of which lines changed. Algorithm-first, AI-optional, works with or without git.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

Differens

CInpm versionLicense: MIT

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.

📖 Documentation: full API reference, guides, and architecture deep-dive.

How it works (the short version)

  1. Parse both sides into a structured tree using tree-sitter
  2. Match nodes between trees with a top-down/bottom-up algorithm (GumTree lineage)
  3. Emit a typed edit script: Insert, Delete, Update, Move
  4. 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 it handles

What changedWhat you get
Function renamedrenamed function parse_config to load_config
Code moved across filesmoved function validate from utils.ts to validators.ts
Class addedadded class RetryPolicy
Config key changedchanged database.pool.max from 10 to 25
Whitespace onlyreformatted 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.

Install

npm install -g differens

Or run it without installing:

npx differens

Node 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.

Use it as a library

The engine is published in pieces, so you can take the matching core without the tree-sitter grammars, or the narration without git.

PackageWhat it gives you
@ossl-dev/differens-corediffTrees, the node model, typed edit scripts. No dependencies.
@ossl-dev/differens-tiersTurns source, config and markup into trees the core can match. Brings the grammars.
@ossl-dev/differens-narrateEdit script to sentences, markdown, JSON, or the compact model format.
@ossl-dev/differens-gitWorking tree, commit range and directory diffs; the diff driver.
@ossl-dev/differens-correlateFinds code that moved between files.
import{diffTrees,treeFromValue}from"@ossl-dev/differens-core";constbefore=treeFromValue({retries: 3,host: "a.example"});constafter=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 differens

The 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.

Usage

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=llm

diff is kept as an explicit alias (differens diff a.ts b.ts).

Output formats

FlagUse
(default)Terminal, one line per change with scope: changed value of port from 3000 to 8080 in object root
--format=jsonRaw SemanticChange array, for tooling
--format=markdownRolled-up summary, for PR descriptions
--format=llmDense line format for AI tools: one line per change, with source line numbers. Roughly 15x smaller than the git diff it replaces
--format=ndjsonOne JSON object per changed file, streamed in input order as results land

differens.toml or .differensrc.json in the repo root sets the default format and the git driver extension list; flags override it.

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.

Other commands

differens languages # what's supported: semantic vs generic per language
differens install-git-driver # register as a git diff driver, writes .gitattributes
differens --help # usage
differens --version # version number

Project structure

differens/
├── 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

Design principles

  • 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.

Status

Milestone 0 shipped: GumTree-lineage matching core (53-bit Merkle hashing, postorder index, Dice bottom-up, LIS-minimised moves, full content verification behind every hash match), JSON/YAML/TOML/INI/env adapters, tree-sitter code adapter with extractors for sixteen languages (TypeScript/JavaScript, Python, Rust, Go, C, C++, Java, Ruby, PHP, Swift, Kotlin, C#, Scala, Lua, shell), git integration (working tree, commit ranges, commit pairs, batched blob reads, self-writing .gitattributes), directory diffing with cross-directory rename detection, a cross-file correlator, streaming ndjson output, and the narration engine with terminal/markdown/json/llm output. Per-file diffs run on a process pool, and a content-addressed parse cache reuses trees within a run. 420 tests, zero failures. Published on npm as differens, runs on Node.

Prior art worth reading before contributing

  • 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

License

MIT

About

A diff engine that tells you what actually happened to your code, moved, renamed, extracted, reformatted, instead of which lines changed. Algorithm-first, AI-optional, works with or without git.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Differens

CInpm versionLicense: MIT

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.

📖 Documentation: full API reference, guides, and architecture deep-dive.

How it works (the short version)

  1. Parse both sides into a structured tree using tree-sitter
  2. Match nodes between trees with a top-down/bottom-up algorithm (GumTree lineage)
  3. Emit a typed edit script: Insert, Delete, Update, Move
  4. 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 it handles

What changedWhat you get
Function renamedrenamed function parse_config to load_config
Code moved across filesmoved function validate from utils.ts to validators.ts
Class addedadded class RetryPolicy
Config key changedchanged database.pool.max from 10 to 25
Whitespace onlyreformatted 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.

Install

npm install -g differens

Or run it without installing:

npx differens

Node 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.

Use it as a library

The engine is published in pieces, so you can take the matching core without the tree-sitter grammars, or the narration without git.

PackageWhat it gives you
@ossl-dev/differens-corediffTrees, the node model, typed edit scripts. No dependencies.
@ossl-dev/differens-tiersTurns source, config and markup into trees the core can match. Brings the grammars.
@ossl-dev/differens-narrateEdit script to sentences, markdown, JSON, or the compact model format.
@ossl-dev/differens-gitWorking tree, commit range and directory diffs; the diff driver.
@ossl-dev/differens-correlateFinds code that moved between files.
import{diffTrees,treeFromValue}from"@ossl-dev/differens-core";constbefore=treeFromValue({retries: 3,host: "a.example"});constafter=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 differens

The 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.

Usage

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=llm

diff is kept as an explicit alias (differens diff a.ts b.ts).

Output formats

FlagUse
(default)Terminal, one line per change with scope: changed value of port from 3000 to 8080 in object root
--format=jsonRaw SemanticChange array, for tooling
--format=markdownRolled-up summary, for PR descriptions
--format=llmDense line format for AI tools: one line per change, with source line numbers. Roughly 15x smaller than the git diff it replaces
--format=ndjsonOne JSON object per changed file, streamed in input order as results land

differens.toml or .differensrc.json in the repo root sets the default format and the git driver extension list; flags override it.

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.

Other commands

differens languages # what's supported: semantic vs generic per language
differens install-git-driver # register as a git diff driver, writes .gitattributes
differens --help # usage
differens --version # version number

Project structure

differens/
├── 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

Design principles

  • 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.

Status

Milestone 0 shipped: GumTree-lineage matching core (53-bit Merkle hashing, postorder index, Dice bottom-up, LIS-minimised moves, full content verification behind every hash match), JSON/YAML/TOML/INI/env adapters, tree-sitter code adapter with extractors for sixteen languages (TypeScript/JavaScript, Python, Rust, Go, C, C++, Java, Ruby, PHP, Swift, Kotlin, C#, Scala, Lua, shell), git integration (working tree, commit ranges, commit pairs, batched blob reads, self-writing .gitattributes), directory diffing with cross-directory rename detection, a cross-file correlator, streaming ndjson output, and the narration engine with terminal/markdown/json/llm output. Per-file diffs run on a process pool, and a content-addressed parse cache reuses trees within a run. 420 tests, zero failures. Published on npm as differens, runs on Node.

Prior art worth reading before contributing

  • 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

License

MIT

About

A diff engine that tells you what actually happened to your code, moved, renamed, extracted, reformatted, instead of which lines changed. Algorithm-first, AI-optional, works with or without git.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Differens

CInpm versionLicense: MIT

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.

📖 Documentation: full API reference, guides, and architecture deep-dive.

How it works (the short version)

  1. Parse both sides into a structured tree using tree-sitter
  2. Match nodes between trees with a top-down/bottom-up algorithm (GumTree lineage)
  3. Emit a typed edit script: Insert, Delete, Update, Move
  4. 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 it handles

What changedWhat you get
Function renamedrenamed function parse_config to load_config
Code moved across filesmoved function validate from utils.ts to validators.ts
Class addedadded class RetryPolicy
Config key changedchanged database.pool.max from 10 to 25
Whitespace onlyreformatted 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.

Install

npm install -g differens

Or run it without installing:

npx differens

Node 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.

Use it as a library

The engine is published in pieces, so you can take the matching core without the tree-sitter grammars, or the narration without git.

PackageWhat it gives you
@ossl-dev/differens-corediffTrees, the node model, typed edit scripts. No dependencies.
@ossl-dev/differens-tiersTurns source, config and markup into trees the core can match. Brings the grammars.
@ossl-dev/differens-narrateEdit script to sentences, markdown, JSON, or the compact model format.
@ossl-dev/differens-gitWorking tree, commit range and directory diffs; the diff driver.
@ossl-dev/differens-correlateFinds code that moved between files.
import{diffTrees,treeFromValue}from"@ossl-dev/differens-core";constbefore=treeFromValue({retries: 3,host: "a.example"});constafter=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 differens

The 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.

Usage

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=llm

diff is kept as an explicit alias (differens diff a.ts b.ts).

Output formats

FlagUse
(default)Terminal, one line per change with scope: changed value of port from 3000 to 8080 in object root
--format=jsonRaw SemanticChange array, for tooling
--format=markdownRolled-up summary, for PR descriptions
--format=llmDense line format for AI tools: one line per change, with source line numbers. Roughly 15x smaller than the git diff it replaces
--format=ndjsonOne JSON object per changed file, streamed in input order as results land

differens.toml or .differensrc.json in the repo root sets the default format and the git driver extension list; flags override it.

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.

Other commands

differens languages # what's supported: semantic vs generic per language
differens install-git-driver # register as a git diff driver, writes .gitattributes
differens --help # usage
differens --version # version number

Project structure

differens/
├── 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

Design principles

  • 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.

Status

Milestone 0 shipped: GumTree-lineage matching core (53-bit Merkle hashing, postorder index, Dice bottom-up, LIS-minimised moves, full content verification behind every hash match), JSON/YAML/TOML/INI/env adapters, tree-sitter code adapter with extractors for sixteen languages (TypeScript/JavaScript, Python, Rust, Go, C, C++, Java, Ruby, PHP, Swift, Kotlin, C#, Scala, Lua, shell), git integration (working tree, commit ranges, commit pairs, batched blob reads, self-writing .gitattributes), directory diffing with cross-directory rename detection, a cross-file correlator, streaming ndjson output, and the narration engine with terminal/markdown/json/llm output. Per-file diffs run on a process pool, and a content-addressed parse cache reuses trees within a run. 420 tests, zero failures. Published on npm as differens, runs on Node.

Prior art worth reading before contributing

  • 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

License

MIT

About

A diff engine that tells you what actually happened to your code, moved, renamed, extracted, reformatted, instead of which lines changed. Algorithm-first, AI-optional, works with or without git.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

Differens

CInpm versionLicense: MIT

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.

📖 Documentation: full API reference, guides, and architecture deep-dive.

How it works (the short version)

  1. Parse both sides into a structured tree using tree-sitter
  2. Match nodes between trees with a top-down/bottom-up algorithm (GumTree lineage)
  3. Emit a typed edit script: Insert, Delete, Update, Move
  4. 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 it handles

What changedWhat you get
Function renamedrenamed function parse_config to load_config
Code moved across filesmoved function validate from utils.ts to validators.ts
Class addedadded class RetryPolicy
Config key changedchanged database.pool.max from 10 to 25
Whitespace onlyreformatted 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.

Install

npm install -g differens

Or run it without installing:

npx differens

Node 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.

Use it as a library

The engine is published in pieces, so you can take the matching core without the tree-sitter grammars, or the narration without git.

PackageWhat it gives you
@ossl-dev/differens-corediffTrees, the node model, typed edit scripts. No dependencies.
@ossl-dev/differens-tiersTurns source, config and markup into trees the core can match. Brings the grammars.
@ossl-dev/differens-narrateEdit script to sentences, markdown, JSON, or the compact model format.
@ossl-dev/differens-gitWorking tree, commit range and directory diffs; the diff driver.
@ossl-dev/differens-correlateFinds code that moved between files.
import{diffTrees,treeFromValue}from"@ossl-dev/differens-core";constbefore=treeFromValue({retries: 3,host: "a.example"});constafter=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 differens

The 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.

Usage

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=llm

diff is kept as an explicit alias (differens diff a.ts b.ts).

Output formats

FlagUse
(default)Terminal, one line per change with scope: changed value of port from 3000 to 8080 in object root
--format=jsonRaw SemanticChange array, for tooling
--format=markdownRolled-up summary, for PR descriptions
--format=llmDense line format for AI tools: one line per change, with source line numbers. Roughly 15x smaller than the git diff it replaces
--format=ndjsonOne JSON object per changed file, streamed in input order as results land

differens.toml or .differensrc.json in the repo root sets the default format and the git driver extension list; flags override it.

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.

Other commands

differens languages # what's supported: semantic vs generic per language
differens install-git-driver # register as a git diff driver, writes .gitattributes
differens --help # usage
differens --version # version number

Project structure

differens/
├── 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

Design principles

  • 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.

Status

Milestone 0 shipped: GumTree-lineage matching core (53-bit Merkle hashing, postorder index, Dice bottom-up, LIS-minimised moves, full content verification behind every hash match), JSON/YAML/TOML/INI/env adapters, tree-sitter code adapter with extractors for sixteen languages (TypeScript/JavaScript, Python, Rust, Go, C, C++, Java, Ruby, PHP, Swift, Kotlin, C#, Scala, Lua, shell), git integration (working tree, commit ranges, commit pairs, batched blob reads, self-writing .gitattributes), directory diffing with cross-directory rename detection, a cross-file correlator, streaming ndjson output, and the narration engine with terminal/markdown/json/llm output. Per-file diffs run on a process pool, and a content-addressed parse cache reuses trees within a run. 420 tests, zero failures. Published on npm as differens, runs on Node.

Prior art worth reading before contributing

  • 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

License

MIT

About

A diff engine that tells you what actually happened to your code, moved, renamed, extracted, reformatted, instead of which lines changed. Algorithm-first, AI-optional, works with or without git.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages