Skip to main content

A console prose linter — readability checks plus opt-in AI-writing / de-slop checks. Like flake8, but for English.

Project description

nabokov

A console linter for English prose: readability, and the tells of AI writing. One warning per line, toggled like flake8.

Python Built with spaCy License: MIT

Code gets review. Prose gets a shrug. nabokov catches hard sentences, adverbs, passive voice, wordy phrases, and qualifiers. Every report carries a readability grade. Opt-in checks catch the tells of AI writing: puffery like delve, chatbot filler, em-dash pileups, flat robotic rhythm. Findings print as warnings you pipe into an editor or CI. Each check is a rule with its own code, so you toggle them like flake8.

Try it

uvx nabokov draft.md          # one command, no setup; fetches the model on first run

Point it at a paragraph of AI slop and it answers:

$ nabokov --ai release-notes.md
release-notes.md:1:14: NB502 AI tell: puffery 'leverages'
release-notes.md:1:51: NB502 AI tell: puffery 'transformative'
release-notes.md:1:75: NB501 AI tell: negation-contrast 'It's not just an update, it's'
release-notes.md:1:107: NB502 AI tell: puffery 'paradigm'
release-notes.md:1:127: NB302 passive voice: 'was celebrated by the whole team'

5 issues in 1 file

Or try it in your browser → (no install).

Why

Code has linters, but prose rarely does. Style guides live in people's heads. nabokov moves them into your terminal. It points at the sentence that reads hard and says why.

Install

Requires Python 3.12 or newer.

uv tool install nabokov       # or: pipx install nabokov
nabokov download-model        # one-time: fetch the spaCy model (en_core_web_sm)
nabokov draft.md

That spaCy model is why nabokov is accurate. It reads grammar, not patterns: it tells a verb from a noun and follows sentence structure, so it raises far fewer false alarms than a regex linter. nabokov fetches it on the first run, and nabokov download-model does it up front. For local development, uv sync pulls everything.

Usage

nabokov draft.md                 # colored report for humans
nabokov --format=flake8 x.md     # path:line:col: CODE message
cat notes.txt | nabokov -        # read from stdin
nabokov docs/                    # walk a directory of .txt / .md / .html files
nabokov --max-grade 9 x.md       # exit non-zero if the grade goes over 9
nabokov --target essay draft.md  # judge against the ESSAY reading level
nabokov --select NB302 x.md      # run one rule
nabokov --ignore NB301 x.md      # skip a rule
nabokov --stats x.md             # document metrics: grade, sentence length, burstiness
nabokov --hotspots x.md          # rank the worst paragraphs, after the findings
nabokov --list-rules             # print every code

--stats prints one metrics line per file (also in --format json as summary). Burstiness is the sentence-length coefficient of variation. High means varied, human rhythm. Low means flat and machine-uniform. Diff it between two drafts to catch a rewrite that got polished flat.

A second register: line reports three more numbers — noun share, pronoun rate, and the temporal share of connectives. No rule reads them and --score ignores them: the research behind them gives directions without thresholds, so they are worth diffing between two drafts and worthless as targets. See docs/RULES.md.

nabokov reads plain text, Markdown, and HTML. For .md and .html it blanks the markup: code, tags, and link URLs. It then checks only the visible prose, so findings point at real writing. For stdin, --stdin-display-name draft.md sets the type.

Exit codes follow flake8: 0 when clean, 1 on findings, 2 on a usage error.

Output formats

Choose one with --format:

  • Color (--format=color) highlights snippets and adds a grade summary. It is the terminal default.
  • Flake8 (--format=flake8) prints one finding per line, for editors and CI.
  • JSON (--format=json) returns diagnostics plus the document grade.
  • GitHub (--format=github) emits workflow annotations for GitHub Actions.

Suggested fixes

Most findings come with the fix, tagged by how mechanically it applies. An arrow is a substitution you can make as-is; try: is a draft you still have to land.

$ nabokov --ai draft.md
draft.md:1:18: NB401 wordy: 'in order to' → to
draft.md:2:12: NB302 passive voice: 'was written by the team' try: The team wrote the report
draft.md:3:20: NB510 AI tell: weak intensifier 'very' — cut it or be specific → delete it

Where the phrase sits decides the tier. Cut Moreover, off the front of a sentence and you strand the comma and lose the capital, so that one reports as a rewrite. Mid-sentence, the same cut is clean. NB303 never offers to delete a negated hedge: dropping "I don't think" inverts the claim instead of softening it.

Nothing here is a thesaurus. We wrote the alternatives by hand, over the closed sets nabokov already flags. A general synonym source returns the wrong sense and the wrong register. One bad suggestion costs more trust than a missing one buys. --format json carries suggestion and applicability on every diagnostic — the form the agent skills and the bot read.

Where to start: --hotspots

The report tells you what is wrong. --score tells you how bad the whole draft is. Neither answers the question you have with ten minutes to spend.

$ nabokov --ai --hotspots draft.md
Hotspots (draft.md) — worst paragraphs first:
  1. line 5-7: 17 findings in 34 words (density 88.2)
       NB502×11 NB201 NB302 NB401 NB505 NB510 NB520
       Moreover, the platform leverages a robust tapestry of very…

It ranks by findings per word, not raw count, so a long paragraph doesn't win just by being long. No new signal goes into it — only the findings the rules already produced, weighted by severity.

Rules

Run nabokov --list-rules to see them all. The full reference lives in docs/RULES.md.

Code What it flags
NB201 / NB202 The very hard and hard reading levels.
NB203 A main clause buried after 20+ words of build-up (advisory).
NB301 Adverbs.
NB302 Passive voice.
NB303 Qualifiers and hedges.
NB304 Nominalizations behind light verbs: "came to an agreement" → agreed.
NB305 Dummy subjects: "There are many resorts in Colorado" → "Colorado has…".
NB306 Repeated words: "Paris in the the spring".
NB307 Uncomparables: "very unique", "most perfect".
NB308 Claiming the task takes no effort: "simply run", "Installation is easy".
NB309 Acronyms nothing expands: "configure the FQDN" (advisory).
NB310 Orienting by position: "the diagram above", "see below" (advisory).
NB311 Images with no alt text (advisory).
NB312 Link text that says nothing: "click here".
NB313 Headings ending in sentence punctuation: "## Requirements**:**".
NB314 List steps that name the reader: "1. You should click Save" → "Click Save".
NB316 Claims attributed to nobody: "Studies show…", "Experts agree", "It is widely believed that…".
NB317 Invisible or look-alike characters: zero-width spaces, a Cyrillic е inside a Latin word.
NB401 Wordy phrases, with a simpler suggestion.
NB601 Abstract, "empty prose" paragraphs, scored against the Brysbaert concreteness norms (advisory).
NB801 A README that never says what the project is.
NB101 The document grade, reported with --max-grade.

Reading-level targets

One bar does not fit every text. --target sets the level nabokov holds a sentence to (case-insensitive):

  • accessible: plain language; sentences count as hard from grade 8, very hard from 12.
  • normal: the default; hard from grade 10, very hard from 14.
  • technical: docs for expert readers; hard from grade 14, very hard from 18.
  • essay: essays, blog posts, opinion pieces. The TECHNICAL thresholds, plus the loosest style budgets for a writer's voice.
  • social: short-form posts. Plain-language thresholds. Staccato fragments and repeated openers are the genre's voice, not AI tells.
  • email: business email. A high-trust audience, so the tightest style budgets of any target.
nabokov --target technical api-guide.md
nabokov --target essay draft.md

Each target also carries style budgets, counted per 1000 words. Adverbs, passive voice, qualifiers, and wordy phrases stay info within budget. Over budget, they become warnings. To make a target stick, set target in your config instead of passing the flag each run (see Configuration).

Signs of AI writing (opt-in)

nabokov also spots common LLM tells (NB5xx). The lists come from the Wikipedia guide Signs of AI writing and community threads. It catches the it's not X, it's Y construction and puffery like delve or tapestry. Promotional phrases, chatbot filler like Great question!, and overused transitions all trip it. It also flags em-dash and emoji overuse. Rule-of-three fragments, flat sentence rhythm, and repeated openers round it out. It even puts a number on the flat rhythm: the burstiness metric from --stats (see Usage).

These checks stay off by default, because they often flag a writer's own voice. Turn them on with a flag:

nabokov --ai draft.md         # the core checks plus the AI-writing checks
nabokov --ai-only essay.md    # only the AI-writing checks

--ai is shorthand for --extend-select NB5, and --ai-only for --select NB5.

Inclusive terminology (opt-in)

--terminology adds NB315: exclusionary terms that have a settled replacement — whitelistallowlist, master/slaveprimary/replica, sanity check, man hours. Off by default, because this is a policy a project adopts rather than a defect nabokov gets to assert on your behalf.

An entry earns its place by having an agreed replacement, not by someone having objected to the word. crazy and blind spot were drafted and cut for that reason. master on its own is absent too: it is a master's degree, a master copy, and a branch name hard-coded in a million scripts. Only the slave-paired sense is listed.

Pair it with the agent skills

The linter catches the mechanical part. Two sibling skills teach an agent to act on it.

nabokov-editor fixes the findings, then reads for what rules miss: empty sentences, invented detail, hollow closers. Fixes keep your meaning. Big edits wait for your approval.

nabokov-copywriter does the opposite move. A clean draft can still be flat, so this skill adds. You pick a goal (sell, reach, provoke, or build trust), and it rebuilds the draft toward it: rhythm, a concrete scene, a proven structure, a call to action. It works from your real facts and asks when a scene needs a detail it doesn't have, then re-lints so the polish never slides back into slop.

# Claude Code
/plugin marketplace add viewflow/nabokov
/plugin install nabokov@viewflow

# Cursor, Codex, Gemini CLI, and other agents (via the skills CLI)
npx skills add viewflow/nabokov

Then ask your agent to de-slop a file, or to make copy land. Skill details live in skills/nabokov-editor/SKILL.md and skills/nabokov-copywriter/SKILL.md.

The Telegram bot

The linter also lives in Telegram: @nabokov_editor_bot. Behind it is a DeepSeek-powered editor that keeps your voice. It cuts the slop, restores the rhythm, and asks when a fact is missing. The reply carries the AI-likeness score before and after. Editor and copywriter modes; first three texts free. The code lives in bot/.

Configuration

Put settings under [tool.nabokov] in pyproject.toml, or in a .nabokov.toml. nabokov walks up from the current directory to find one. CLI flags win.

[tool.nabokov]
target = "NORMAL"       # ACCESSIBLE | NORMAL | TECHNICAL | ESSAY | SOCIAL | EMAIL
ignore = ["NB301"]      # e.g. stop flagging adverbs
known_acronyms = ["CRD", "RBAC"]   # NB309: your own everyday abbreviations

[tool.nabokov.budgets]  # optional: per-1000-word style budgets (see docs/RULES.md)
NB301 = 20              # adverbs stay advisory (info) up to this density

Suppress one line inline:

This sentence is fine.  <!-- nabokov: ignore NB302 -->

How it works

nabokov scores readability with the Automated Readability Index (ARI). Word characters drive the grade, so nabokov counts no syllables.

The adverb list and the phrase dictionary began as classic lists for plain language. We added extra hedges and more phrase alternatives. A fuller set of irregular participles feeds the passive check.

spaCy handles the parsing. Passive voice reads the auxpass dependency. Adverbs read the part-of-speech tag plus the -ly suffix. The pipeline loads once and runs on every file.

Development

uv run pytest               # the test suite
uv run ruff check .         # lint
uv run ruff format .        # format
uv run pyright              # type-check

The nabokov-editor skill drives the linter inside an agent loop.

License

MIT.

Credits

Inspired by the Hemingway Editor. Parsing uses spaCy. The name is a nod to a writer who cared about sentences.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

nabokov-26.7.10.tar.gz (608.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

nabokov-26.7.10-py3-none-any.whl (354.2 kB view details)

Uploaded Python 3

File details

Details for the file nabokov-26.7.10.tar.gz.

File metadata

  • Download URL: nabokov-26.7.10.tar.gz
  • Upload date:
  • Size: 608.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for nabokov-26.7.10.tar.gz
Algorithm Hash digest
SHA256 2907fb6082e21c552b42bbe851dad8c2b88469e85625e0ad279ba27dbf59f661
MD5 b2411958a29adeabacaba7f7deafe6e8
BLAKE2b-256 421b7b09eac47d9da2a3fab18b8a0c08e9198f59a51c8831e08667f943332a27

See more details on using hashes here.

File details

Details for the file nabokov-26.7.10-py3-none-any.whl.

File metadata

  • Download URL: nabokov-26.7.10-py3-none-any.whl
  • Upload date:
  • Size: 354.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for nabokov-26.7.10-py3-none-any.whl
Algorithm Hash digest
SHA256 f7ba248cbf3aade9c9bc3252a5141c2bb57b1421224eb38a9c8c8d69aebbad65
MD5 4b32ca78a2c83b198ff56f6eb3b4d2cf
BLAKE2b-256 e1c969bf3f80edc4527fc3387a2394bea86c8c5fc491d9b2ebcd8fd92a4ec987

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page