A small, zero-dependency Node tool that computes the writing signals the skill talks about, so you can score docs deterministically and gate them in CI.
The pure-Markdown skill in ../skills/humanizer/SKILL.md needs none of this. This directory is a separate, optional layer for power users and teams. Nothing here runs unless you run it.
For any text it reports:
- Burstiness (coefficient of variation of sentence length) and Goh-Barabasi burstiness
- Type-token ratio and MATTR (length-stable lexical diversity)
- Trigram repetition (the treadmill signal, P43)
- AI-vocabulary tell density (tiered blacklist, mirrors P7 and the tiered vocab in the skill)
- Flesch-Kincaid grade and reading ease
- A transparent weighted 0-100 AI-tell score (higher = more AI smell)
The score is a deterministic proxy: 0.28 low-burstiness + 0.18 low-diversity + 0.14 high-repetition + 0.40 lexical-tells. It is not a trained detector and does not call any network. The /humanizer skill's LLM-estimated score is more holistic; this one is fast and reproducible. Use them together.
Node 18 or newer. No npm install needed.
# Score one file (or - for stdin)
node cli/index.js score README.md
cat draft.md | node cli/index.js score -
# Ignore code fences and quoted examples so they don't inflate the score
node cli/index.js score docs/api.md --ignore-code --ignore-quotes
# Scan a whole tree, or a list of files
node cli/index.js scan docs/
node cli/index.js scan README.md docs/faq.md
# Show the before/after delta
node cli/index.js compare --before draft.md --after final.md
# Fail if the rewrite dropped a fact
node cli/index.js compare --before draft.md --after final.md --check-facts
# JSON for tooling
node cli/index.js score README.md --json--check-facts compares the hard tokens in the two files and exits 1 if anything in --before
is missing from --after:
| Kind | Examples | Normalized so that |
|---|---|---|
| numbers | 2200, 5,400, 12.5 |
5,400 and 5400 are the same fact |
| percentages | 59%, 12.5 % |
spacing does not matter |
| dates | 2026-04-16, April 16, 2026, 16 April 2026 |
all three are one token |
| versions | v0.7.0, 0.7.0 |
the v prefix does not matter, and dropping it does not read as a loss |
| URLs | https://example.com/a?b=1 |
trailing sentence punctuation is trimmed, balanced brackets are kept |
| names | AWS, RDS, P18 |
any run of two or more capitals and digits, minus GitHub alert markers and shouted words |
Facts are read from the raw text, so a number inside a code fence still counts even under
--ignore-code. A fact the rewrite moved into a link still counts. Adding a fact is never
reported, because adding detail is a writing choice.
This checks that every hard token survived. It is not a proof of factual equivalence.
- Swapped values. The check is set-based. Turning "7 errors in 2 regions" into "2 errors in 7 regions" keeps both numbers, so it passes. It catches deletions and edits, not reorderings.
- Facts with no hard token. "most customers" becoming "all customers" changes the claim and contains nothing to extract.
- Meaning. A number kept in a sentence that now says the opposite still counts as present.
Fail the build when docs get too AI-flavored, or only when they regress against a saved baseline:
# Hard threshold
node cli/index.js scan docs/ --fail-above 40
# Save a baseline once, then fail only on regressions (existing debt is grandfathered)
node cli/index.js scan docs/ --baseline .humanizer-baseline.json --write-baseline
node cli/index.js scan docs/ --baseline .humanizer-baseline.json --fail-on-regressionExit codes: 0 pass, 1 gate failed, 2 usage or IO error.
A reusable composite action lives at .github/actions/humanizer-gate:
- uses: Aboudjem/humanizer-skill/.github/actions/humanizer-gate@main
with:
path: docs/
fail-above: '40'For a fast local gate (the GitHub Action above is the CI-side backstop), the repo ships a .pre-commit-hooks.yaml for the Python pre-commit framework. It wraps the same zero-dependency CLI, language: script (no install step, just Node), and re-scans the repo on every commit that touches a Markdown or text file.
Add this to your own repo's .pre-commit-config.yaml:
repos:
- repo: https://github.com/Aboudjem/humanizer-skill
rev: main # pin to a tagged release or commit SHA in production; `pre-commit autoupdate` will not update a mutable ref
hooks:
- id: humanizer-scanThen pre-commit install once. The hook fails the commit if any scanned file scores above 40; adjust the threshold by overriding entry in your own config, e.g. entry: cli/index.js scan . --fail-above 60.
Not using the Python pre-commit framework? Husky (the most common JS-native alternative) works too. The CLI is not published to npm today (see below), so vendor it, for example as a git submodule at tools/humanizer-skill, then add one line to .husky/pre-commit:
node tools/humanizer-skill/cli/index.js scan . --fail-above 40cd cli && node --test64 tests, no dependencies. Covers tokenization, every metric, the composite score and its per-signal breakdown, code/quote masking, the vocabulary layer, fact extraction and the fact-loss diff, multi-path scanning, the packaged bin (shebang, executable bit, and that no shipped module requires a file outside the tarball), and CLI exit codes (threshold gate, baseline regression, fact check, bad input).
Running the CLI needs Node 18 or newer, and CI runs the suite on both 18 and 20. Contributing or publishing needs Node 20.19 or newer, because eslint 10 does not run on 18.
cd cli && npm install && npm run lintESLint is a devDependency only, used for local and CI linting; it never ships with the published package and does not affect the CLI's zero-runtime-dependency claim.