Library / SDK
spencermountain/compromise avatar
spencermountain/compromise

compromise: a JavaScript NLP library that makes limited, predictable decisions

modest natural-language processing

12,157 stars668 forksJavaScriptMIT

At a glance

What is it?
compromise turns text into data with part-of-speech tagging, named-entity recognition and a small query language, and its README is explicit that it is not as smart as you would think. Here is what it does, how to install it, and where it stops being the right tool.
Who is it for?
compromise fits scripts, browser tools, tests and pipelines that need tags, entities or number and verb transforms on English text without a model file or a service call. It does not fit work that needs dependency parsing, high-accuracy entity typing, or languages other than English, which live in separate repositories such as fr-compromise and de-compromise.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 2 days ago.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 16, 2026, and from our analysis. They are not legal advice.

DEEP OPEN-SOURCE ANALYSIS

What compromise solves, and the line it draws around itself

Most text processing starts as a regex problem and slowly becomes a parsing problem. You want the verbs in a sentence, the places in a paragraph, or the number written as words turned into an integer. Regular expressions work until the text stops being regular, and then a full statistical NLP stack arrives with a model download, a Python runtime or an API key. compromise sits between those two positions. It is a JavaScript library, published on npm as compromise, that reads English text and exposes it as an object you can query and edit.

The README describes the project as modest natural language processing and says it "tries its best" to turn text into data. It also states plainly that it "makes limited and sensible decisions" and that "it's not as smart as you'd think." That framing is the useful part. The library is aimed at developers who want deterministic, inspectable output on ordinary English sentences, not at teams building a general-purpose NLP service. If your input is user-generated text in many languages, or if you need syntactic structure, this is the wrong shelf.

How the nlp() document model works

The entry point is nlp(), which takes a string and returns a document object. That object is not a string and not a token array you index directly. It is a selection model, closer to a DOM query than to a parser tree. You call methods such as .verbs(), .places(), .numbers() or .match() to narrow the document to a subset, and then call .text() or .json() to read the result.

The README's first example shows the pattern. A document is built from a sentence, a selection of verbs is made, a transform is applied, and .text() returns the rewritten sentence. Because the selection is the unit of work, transforms compose: you select, edit, select something else, edit again, and read once at the end. The .json() output is a plain array of objects with a text field and a terms array, where each term carries fields such as normal and, when a plugin computes them, syllables. Nothing in that structure requires a model file or a network call.

The query language is the second half of the mechanism. The README shows doc.has('simon says #Verb') and doc.match('the #Adjective of times'). Tokens prefixed with # are part-of-speech tags, so a query mixes literal words with grammatical slots. .has() returns a boolean; .match() returns a narrower document. That is a small language, and the README links to a match documentation page rather than reproducing the full grammar, so the tag vocabulary is something you check in the docs rather than memorize from the README.

Installing compromise and running a first real query

The README gives one install line, npm install compromise. The package is ESM-first: package.json sets "type": "module" and points the main and module fields at ./src/three.js, with a separate CommonJS build under builds/three/compromise-three.cjs exposed through the exports map. If your project is still CommonJS, the require path resolves to the .cjs build rather than to src.

bash
npm install compromise

A first script can stay very small. The README's opening example selects the verbs in a sentence, shifts them to past tense, and prints the result.

js
import nlp from 'compromise'

let doc = nlp('she sells seashells by the seashore.')
doc.verbs().toPastTense()
doc.text()
// 'she sold seashells by the seashore.'

What you should see is the sentence with sells rewritten as sold. Because doc is mutated by the transform, calling doc.text() after the call is what returns the new string; there is no separate return value to capture.

Queries use the same object. The README shows a boolean check with a tag slot, which is useful for routing logic without extracting anything.

js
if (doc.has('simon says #Verb')) {
  return true
}

For extraction, .match() narrows the document and .text() reads it back. The README's example matches an adjective between two fixed words.

js
let doc = nlp(entireNovel)
doc.match('the #Adjective of times').text()
// "the blurst of times?"

Plugins extend the document prototype. The README installs compromise-speech and calls a compute step before reading JSON, which is how extra per-term fields appear in the output.

js
import plg from 'compromise-speech'
nlp.extend(plg)

let doc = nlp('Milwaukee has certainly had its share of visitors..')
doc.compute('syllables')
doc.places().json()
/*
[{
  "text": "Milwaukee",
  "terms": [{
    "normal": "milwaukee",
    "syllables": ["mil", "wau", "kee"]
  }]
}]
*/

The JSON above is copied from the README. Note that the syllables field exists only because the plugin was extended and compute was called; without those two steps the same .json() call returns the standard term fields. That ordering matters, and it is the most common place a first attempt goes wrong.

Contractions, numbers and the transforms that justify the library

The most convincing examples in the README are the ones where text stops behaving like text. Contractions are handled implicitly: a document built from "we're not gonna take it.." answers true to both doc.has('gonna') and doc.has('going to'), and doc.contractions().expand() rewrites the sentence as "we are not going to take it..". That is the brittle-parser problem the README names directly, and the implicit equivalence between gonna and going to is the feature that solves it.

Numbers go the other way. A document built from 'ninety five thousand and fifty two' can be selected with .numbers(), passed to .add(20), and read back as 'ninety five thousand and seventy two'. The library is doing arithmetic on a spelled-out number and then re-rendering it as words. That is a narrow capability, and it is a good test of whether the library fits your problem: if your pipeline needs number normalization, this replaces a lookup table you would otherwise maintain by hand.

The trade-off is that these transforms are English-specific and rule-driven. The README lists separate repositories for French, German, Italian and Spanish, which tells you the core package is not multilingual. A rule-driven approach also means unusual spellings, dialect forms or heavy slang may simply not be covered, and the library will return the input unchanged rather than flagging that it did not understand.

Where compromise is the wrong tool

The README's own warning is the honest starting point: it is not as smart as you would think. Anything that depends on statistical confidence is out of scope. There is no probability attached to a tag, no confidence score to threshold, and no way to ask the library how sure it is about an entity. If your application needs to route low-confidence cases to a human, this library does not give you the signal to do that.

Entity recognition is the sharper limitation. The README demonstrates .places() returning Milwaukee, which is a place name the library recognizes. It does not claim to resolve ambiguous names, distinguish a person from a company, or handle entities it has not seen. For a corpus of product names, internal codenames or non-English proper nouns, expect misses, and expect them silently.

Language coverage is the second boundary. The README points to fr-compromise, de-compromise, it-compromise and es-compromise as separate projects, so the package installed by npm install compromise is the English one. A multilingual product needs those repositories or a different approach entirely. Finally, the README does not document rollback behaviour for plugin or version changes, so pinning is a decision you make on your own rather than something the project prescribes.

How it compares to spaCy and wink-nlp

spaCy is the usual alternative, and the difference is architectural rather than a matter of quality. spaCy loads trained statistical models and returns documents with token attributes, dependency arcs and named entities, and it runs in Python. compromise ships as JavaScript source with no model download, and its output is a queryable document with a small tag set. If you need dependency structure or entity types with confidence, spaCy is the tool that provides them and compromise is not.

wink-nlp is closer in shape: a JavaScript NLP library with its own model and a token-centric API. The practical distinction is packaging and weight. compromise is published as ESM source with a CommonJS build, so it fits a bundler-based front end with little ceremony, and its query language is string-based rather than method-chained over a token index. Which one you pick depends on whether you want a query string like 'the #Adjective of times' or explicit iteration over tokens with model-backed annotations.

None of these three is a drop-in for the others. The choice is really about whether you can accept rule-driven, English-only output in exchange for zero model files and a small API surface.

Maintenance, licence and the cost of upgrading

The repository is not archived, and the last push was on 2026-09-14, with v14.17.0 released on 2026-09-10. That is a recent release cadence rather than a stalled project, and the changelog.md at the repository root is where the project records what changed between versions.

The version numbers matter for upgrade planning. The package is at 14.x, so the public API has been through many major bumps, and the package.json exports map shows multiple entry points: the root, plus ./tokenize, ./one, ./two, ./three and ./misc, each with its own type definitions. A major upgrade can change which entry point you should import from, so reading changelog.md before bumping is cheaper than discovering the change at runtime.

The licence is MIT, which permits commercial use and modification provided the copyright notice and permission notice are included. That is a permissive arrangement, but it is not legal advice; if your organization has a licence review process, route it through that. The repository also contains an llms.txt file at the root, which suggests the project publishes machine-readable guidance alongside its documentation.

Editorial conclusion

compromise fits scripts, browser tools, tests and pipelines that need tags, entities or number and verb transforms on English text without a model file or a service call. It does not fit work that needs dependency parsing, high-accuracy entity typing, or languages other than English, which live in separate repositories such as fr-compromise and de-compromise. Before adopting it, run the exact sentences you care about through nlp() and inspect the .json() output, because the README states it makes limited and sensible decisions rather than promising accuracy, and the repository does not document a rollback path for plugin or version changes.

Frequently asked questions

How does compromise work?

You pass a string to nlp() and get back a document object. You then select a subset with methods such as .verbs(), .places() or .match(), apply transforms, and read the result with .text() or .json(). The README states that it makes limited and sensible decisions rather than using a statistical model.

What is compromise?

compromise is a JavaScript natural-language processing library published on npm, described in its README as modest natural language processing. It handles part-of-speech tagging, named-entity recognition and text transforms such as expanding contractions and adding numbers written as words.

How do you use compromise in a sentence?

The README's opening example builds a document from 'she sells seashells by the seashore.', calls doc.verbs().toPastTense(), and reads doc.text() to get 'she sold seashells by the seashore.' The document is mutated in place, so the transform happens on the object rather than on a returned value.

How do you use compromise?

Install it with npm install compromise, import nlp from 'compromise', and pass a string to nlp(). From there you select parts of the document with .verbs(), .places(), .numbers() or .match() and read them back with .text() or .json().

Which is an example of compromise?

The README's first example is a sentence transformed by selection: nlp('she sells seashells by the seashore.') with doc.verbs().toPastTense() returns 'she sold seashells by the seashore.' Another example matches an adjective slot with doc.match('the #Adjective of times').

Official sources

  1. License: MIT
  2. Project website
  3. README
  4. Releases
  5. spencermountain/compromise on GitHub
Community notes

Community notes