npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

differens

v0.2.0

Published

Semantic diffing: what actually happened to your code, not which lines changed

Readme

Differens

CI npm version License: 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 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.

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.

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

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

| 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 | | --format=ndjson | One 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