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

clean-writing-lint

v0.1.0

Published

Lint prose for AI slop. Checks the machine-checkable subset of Simplified Technical English in markdown, code comments, and release notes.

Readme

clean-writing-lint

Check prose for AI slop. Reports violations per 100 words. Lower is cleaner.

This is the machine-checkable part of writing-systems/ste.md. The style guide tells an agent how to write. This tells you whether it did.

No dependencies. Node 18 or later.

Use it

npx clean-writing-lint README.md docs

Or add it to a project:

npm install --save-dev clean-writing-lint
{
  "scripts": {
    "lint:prose": "cwlint --max-score 2 README.md docs"
  }
}

The command exits 1 when the score goes above --max-score, so it works as a gate in CI. It also exits 1 when a rule set to error fires.

Point it at a file or a directory. It walks a directory for .md, .mdx, and .markdown files. Read from standard input with --stdin.

What it reports

README.md
  22:14  warn  marketing-adjective   "seamless" is a marketing claim. Delete it, or replace it with a measurement.
  31:1   warn  long-sentence         Sentence is 34 words. The limit is 20.
  47:9   warn  passive-voice         Passive voice with a named actor: "is read by". Make it active.
  3 violations, 966 words, score 0.31 per 100 words

Rules:

| Rule | What it catches | |---|---| | long-sentence | More than 20 words in one sentence | | long-paragraph | More than 6 sentences in one paragraph | | semicolon | Semicolons | | em-dash | Em dashes and en dashes | | exclamation | Exclamation marks | | emoji | Emoji in prose | | contraction | Real contractions, not possessives | | passive-voice | "to be" plus a past participle | | ing-main-verb | "is sending" where "sends" works | | nominalization | "perform an analysis" for "analyze" | | phrasal-verb | "spin up", "reach out", "circle back" | | banned-word | utilize, leverage, prior to, in order to | | marketing-adjective | seamless, robust, world-class | | filler | "it is important to note that" | | intensifier | very, really, simply, actually | | banned-construction | "It is not just X, it is Y", "let us dive in" |

Run cwlint --list-rules for the current list.

It never lints code. It masks out fenced blocks, inline code, frontmatter, link targets, URLs, HTML tags, and table rows first. Masking replaces the region with spaces of the same length, so every line and column in the report points at the real source.

Suppress a rule

A style guide has to name the words it bans. So do release notes that quote a bad error message. Write an HTML comment:

<!-- cws-disable marketing-adjective -->
Never write seamless, robust, or world-class.
<!-- cws-enable -->

Four forms, each taking an optional list of rules. With no list, they cover every rule.

| Directive | Covers | |---|---| | <!-- cws-disable-file --> | The whole file | | <!-- cws-disable --> ... <!-- cws-enable --> | Everything between them | | <!-- cws-disable-next-line --> | The line after it | | <!-- cws-disable-line --> | The line it sits on |

A cws-disable with no matching cws-enable runs to the end of the file.

Configure it

Drop a clean-writing.config.json next to your package.json. The command searches upward from the working directory. A cleanWriting key in package.json works too, as does clean-writing.config.js with a default export.

{
  "maxScore": 2,
  "maxSentenceWords": 20,
  "maxParagraphSentences": 6,
  "rules": {
    "intensifier": "off",
    "em-dash": "error"
  },
  "bannedWords": {
    "add": ["synergy", "ideate"],
    "remove": ["ensure"]
  },
  "passiveIgnore": {
    "add": ["converted"]
  },
  "ignore": ["node_modules", "CHANGELOG.md"]
}

Every word list takes add and remove, so you never restate a default list to change one entry. The lists are bannedWords, marketingAdjectives, phrasalVerbs, filler, intensifiers, and passiveIgnore.

Set lintTables to true to check table cells. Set lintBlockquotes to false to skip quoted text.

Use it from code

import { lint } from 'clean-writing-lint';

const result = lint(markdownSource, { maxSentenceWords: 25 });
console.log(result.score, result.counts);

lint takes a decoded string and returns words, sentences, score, counts, violations, errorCount, warnCount, and longestSentence. Each violation carries rule, severity, line, column, index, length, and message.

Limits

The judgment rules of Simplified Technical English need a person. A checker cannot tell you whether a noun is the right technical name, or whether a sentence is true. This covers the mechanical subset, which is where the slop lives.

Passive voice is the weakest rule. English past participles double as adjectives, so "the file is deprecated" reads as passive to a regex. Common cases ship in a skip list, and you can extend it with passiveIgnore.

Sentence splitting breaks on an abbreviation that ends in a period and comes before a capital letter.

Credit

A port of ste-lint.py by Ege Çelebi, MIT, from https://github.com/woosal1337/blog/tree/main/videos/ep01-the-cure-for-ai-slop

Added here: line and column reporting, suppression comments, a config file, per-rule severity, lists that skip the paragraph check, a contraction rule that knows a possessive, an exit code for CI, and UTF-8 reads. That last one is a bug fix. See UPSTREAM_BUG.md in the repository root.

MIT.